1use crate::storage::query::ast::{
43 CompareOp, EdgeDirection, EdgePattern, FieldRef, Filter, GraphPattern, GraphQuery, NodePattern,
44 Projection, PropertyFilter, QueryExpr,
45};
46use crate::storage::schema::Value;
47
48#[derive(Debug, Clone)]
50pub struct GremlinError {
51 pub message: String,
52 pub position: usize,
53}
54
55impl std::fmt::Display for GremlinError {
56 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 write!(f, "Gremlin error at {}: {}", self.position, self.message)
58 }
59}
60
61impl std::error::Error for GremlinError {}
62
63#[derive(Debug, Clone)]
65pub struct GremlinTraversal {
66 pub source: TraversalSource,
68 pub steps: Vec<GremlinStep>,
70}
71
72#[derive(Debug, Clone, PartialEq)]
74pub enum TraversalSource {
75 Graph,
77 Anonymous,
79}
80
81#[derive(Debug, Clone)]
83pub enum GremlinStep {
84 V(Option<String>), E(Option<String>), Out(Option<String>), In(Option<String>), Both(Option<String>), OutE(Option<String>), InE(Option<String>), BothE(Option<String>), OutV, InV, BothV, OtherV, Has(String, Option<GremlinValue>), HasNot(String), HasLabel(String), HasId(String), Where(Box<GremlinTraversal>), Filter(Box<GremlinTraversal>), Dedup, Limit(u64), Skip(u64), Range(u64, u64), Values(Vec<String>), ValueMap(Vec<String>), Id, Label, Properties(Vec<String>), Count, Sum, Min, Max, Mean, Select(Vec<String>), Project(Vec<String>), Path, SimplePath, CyclicPath, Repeat(Box<GremlinTraversal>), Times(u32), Until(Box<GremlinTraversal>), Emit, Union(Vec<GremlinTraversal>), Choose(
136 Box<GremlinTraversal>,
137 Box<GremlinTraversal>,
138 Option<Box<GremlinTraversal>>,
139 ),
140 Coalesce(Vec<GremlinTraversal>), As(String), By(ByModifier), Aggregate(String), Store(String), Group, GroupCount, ToList, ToSet, Next, Fold, }
156
157#[derive(Debug, Clone, PartialEq)]
159pub enum GremlinValue {
160 String(String),
161 Integer(i64),
162 Float(f64),
163 Boolean(bool),
164 Predicate(GremlinPredicate),
165}
166
167#[derive(Debug, Clone, PartialEq)]
169pub enum GremlinPredicate {
170 Eq(Box<GremlinValue>), Neq(Box<GremlinValue>), Lt(Box<GremlinValue>), Lte(Box<GremlinValue>), Gt(Box<GremlinValue>), Gte(Box<GremlinValue>), Between(Box<GremlinValue>, Box<GremlinValue>), Inside(Box<GremlinValue>, Box<GremlinValue>), Outside(Box<GremlinValue>, Box<GremlinValue>), Within(Vec<GremlinValue>), Without(Vec<GremlinValue>), StartingWith(String), EndingWith(String), Containing(String), Regex(String), }
186
187#[derive(Debug, Clone)]
189pub enum ByModifier {
190 Key(String),
191 Traversal(Box<GremlinTraversal>),
192 Order(OrderDirection),
193}
194
195#[derive(Debug, Clone)]
197pub enum OrderDirection {
198 Asc,
199 Desc,
200}
201
202pub struct GremlinParser<'a> {
204 input: &'a str,
205 pos: usize,
206}
207
208impl<'a> GremlinParser<'a> {
209 pub fn new(input: &'a str) -> Self {
211 Self { input, pos: 0 }
212 }
213
214 pub fn parse(input: &str) -> Result<GremlinTraversal, GremlinError> {
216 let mut parser = GremlinParser::new(input);
217 parser.parse_traversal()
218 }
219
220 fn parse_traversal(&mut self) -> Result<GremlinTraversal, GremlinError> {
222 self.skip_whitespace();
223
224 let source = if self.consume_if("g.") {
226 TraversalSource::Graph
227 } else if self.consume_if("__.") {
228 TraversalSource::Anonymous
229 } else if self.consume_if("__") {
230 TraversalSource::Anonymous
232 } else {
233 return Err(self.error("Expected 'g.' or '__' at start of traversal"));
234 };
235
236 let mut steps = Vec::new();
237
238 loop {
240 self.skip_whitespace();
241
242 if self.is_at_end() || self.peek() == Some(')') || self.peek() == Some(',') {
243 break;
244 }
245
246 self.consume_if(".");
248
249 self.skip_whitespace();
250
251 if self.is_at_end() || self.peek() == Some(')') || self.peek() == Some(',') {
252 break;
253 }
254
255 let step = self.parse_step()?;
256 steps.push(step);
257 }
258
259 Ok(GremlinTraversal { source, steps })
260 }
261
262 fn parse_step(&mut self) -> Result<GremlinStep, GremlinError> {
264 let name = self.parse_identifier()?;
265
266 match name.as_str() {
267 "V" => {
269 self.expect('(')?;
270 let id = self.parse_optional_string_arg()?;
271 self.expect(')')?;
272 Ok(GremlinStep::V(id))
273 }
274 "E" => {
275 self.expect('(')?;
276 let id = self.parse_optional_string_arg()?;
277 self.expect(')')?;
278 Ok(GremlinStep::E(id))
279 }
280
281 "out" => {
283 self.expect('(')?;
284 let label = self.parse_optional_string_arg()?;
285 self.expect(')')?;
286 Ok(GremlinStep::Out(label))
287 }
288 "in" => {
289 self.expect('(')?;
290 let label = self.parse_optional_string_arg()?;
291 self.expect(')')?;
292 Ok(GremlinStep::In(label))
293 }
294 "both" => {
295 self.expect('(')?;
296 let label = self.parse_optional_string_arg()?;
297 self.expect(')')?;
298 Ok(GremlinStep::Both(label))
299 }
300 "outE" => {
301 self.expect('(')?;
302 let label = self.parse_optional_string_arg()?;
303 self.expect(')')?;
304 Ok(GremlinStep::OutE(label))
305 }
306 "inE" => {
307 self.expect('(')?;
308 let label = self.parse_optional_string_arg()?;
309 self.expect(')')?;
310 Ok(GremlinStep::InE(label))
311 }
312 "bothE" => {
313 self.expect('(')?;
314 let label = self.parse_optional_string_arg()?;
315 self.expect(')')?;
316 Ok(GremlinStep::BothE(label))
317 }
318 "outV" => {
319 self.expect('(')?;
320 self.expect(')')?;
321 Ok(GremlinStep::OutV)
322 }
323 "inV" => {
324 self.expect('(')?;
325 self.expect(')')?;
326 Ok(GremlinStep::InV)
327 }
328 "bothV" => {
329 self.expect('(')?;
330 self.expect(')')?;
331 Ok(GremlinStep::BothV)
332 }
333 "otherV" => {
334 self.expect('(')?;
335 self.expect(')')?;
336 Ok(GremlinStep::OtherV)
337 }
338
339 "has" => {
341 self.expect('(')?;
342 let key = self.parse_string()?;
343 self.skip_whitespace();
344 let value = if self.consume_if(",") {
345 self.skip_whitespace();
346 Some(self.parse_value()?)
347 } else {
348 None
349 };
350 self.expect(')')?;
351 Ok(GremlinStep::Has(key, value))
352 }
353 "hasNot" => {
354 self.expect('(')?;
355 let key = self.parse_string()?;
356 self.expect(')')?;
357 Ok(GremlinStep::HasNot(key))
358 }
359 "hasLabel" => {
360 self.expect('(')?;
361 let label = self.parse_string()?;
362 self.expect(')')?;
363 Ok(GremlinStep::HasLabel(label))
364 }
365 "hasId" => {
366 self.expect('(')?;
367 let id = self.parse_string()?;
368 self.expect(')')?;
369 Ok(GremlinStep::HasId(id))
370 }
371 "dedup" => {
372 self.expect('(')?;
373 self.expect(')')?;
374 Ok(GremlinStep::Dedup)
375 }
376 "limit" => {
377 self.expect('(')?;
378 let n = self.parse_integer()? as u64;
379 self.expect(')')?;
380 Ok(GremlinStep::Limit(n))
381 }
382 "skip" => {
383 self.expect('(')?;
384 let n = self.parse_integer()? as u64;
385 self.expect(')')?;
386 Ok(GremlinStep::Skip(n))
387 }
388 "range" => {
389 self.expect('(')?;
390 let from = self.parse_integer()? as u64;
391 self.expect(',')?;
392 self.skip_whitespace();
393 let to = self.parse_integer()? as u64;
394 self.expect(')')?;
395 Ok(GremlinStep::Range(from, to))
396 }
397
398 "values" => {
400 self.expect('(')?;
401 let keys = self.parse_string_list()?;
402 self.expect(')')?;
403 Ok(GremlinStep::Values(keys))
404 }
405 "valueMap" => {
406 self.expect('(')?;
407 let keys = self.parse_string_list()?;
408 self.expect(')')?;
409 Ok(GremlinStep::ValueMap(keys))
410 }
411 "id" => {
412 self.expect('(')?;
413 self.expect(')')?;
414 Ok(GremlinStep::Id)
415 }
416 "label" => {
417 self.expect('(')?;
418 self.expect(')')?;
419 Ok(GremlinStep::Label)
420 }
421 "properties" => {
422 self.expect('(')?;
423 let keys = self.parse_string_list()?;
424 self.expect(')')?;
425 Ok(GremlinStep::Properties(keys))
426 }
427 "count" => {
428 self.expect('(')?;
429 self.expect(')')?;
430 Ok(GremlinStep::Count)
431 }
432 "sum" => {
433 self.expect('(')?;
434 self.expect(')')?;
435 Ok(GremlinStep::Sum)
436 }
437 "min" => {
438 self.expect('(')?;
439 self.expect(')')?;
440 Ok(GremlinStep::Min)
441 }
442 "max" => {
443 self.expect('(')?;
444 self.expect(')')?;
445 Ok(GremlinStep::Max)
446 }
447 "mean" => {
448 self.expect('(')?;
449 self.expect(')')?;
450 Ok(GremlinStep::Mean)
451 }
452 "select" => {
453 self.expect('(')?;
454 let labels = self.parse_string_list()?;
455 self.expect(')')?;
456 Ok(GremlinStep::Select(labels))
457 }
458 "project" => {
459 self.expect('(')?;
460 let keys = self.parse_string_list()?;
461 self.expect(')')?;
462 Ok(GremlinStep::Project(keys))
463 }
464 "path" => {
465 self.expect('(')?;
466 self.expect(')')?;
467 Ok(GremlinStep::Path)
468 }
469 "simplePath" => {
470 self.expect('(')?;
471 self.expect(')')?;
472 Ok(GremlinStep::SimplePath)
473 }
474 "cyclicPath" => {
475 self.expect('(')?;
476 self.expect(')')?;
477 Ok(GremlinStep::CyclicPath)
478 }
479
480 "repeat" => {
482 self.expect('(')?;
483 let inner = self.parse_inner_traversal()?;
484 self.expect(')')?;
485 Ok(GremlinStep::Repeat(Box::new(inner)))
486 }
487 "times" => {
488 self.expect('(')?;
489 let n = self.parse_integer()? as u32;
490 self.expect(')')?;
491 Ok(GremlinStep::Times(n))
492 }
493 "until" => {
494 self.expect('(')?;
495 let inner = self.parse_inner_traversal()?;
496 self.expect(')')?;
497 Ok(GremlinStep::Until(Box::new(inner)))
498 }
499 "emit" => {
500 self.expect('(')?;
501 self.expect(')')?;
502 Ok(GremlinStep::Emit)
503 }
504
505 "as" => {
507 self.expect('(')?;
508 let label = self.parse_string()?;
509 self.expect(')')?;
510 Ok(GremlinStep::As(label))
511 }
512 "aggregate" => {
513 self.expect('(')?;
514 let label = self.parse_string()?;
515 self.expect(')')?;
516 Ok(GremlinStep::Aggregate(label))
517 }
518 "store" => {
519 self.expect('(')?;
520 let label = self.parse_string()?;
521 self.expect(')')?;
522 Ok(GremlinStep::Store(label))
523 }
524 "group" => {
525 self.expect('(')?;
526 self.expect(')')?;
527 Ok(GremlinStep::Group)
528 }
529 "groupCount" => {
530 self.expect('(')?;
531 self.expect(')')?;
532 Ok(GremlinStep::GroupCount)
533 }
534
535 "toList" => {
537 self.expect('(')?;
538 self.expect(')')?;
539 Ok(GremlinStep::ToList)
540 }
541 "toSet" => {
542 self.expect('(')?;
543 self.expect(')')?;
544 Ok(GremlinStep::ToSet)
545 }
546 "next" => {
547 self.expect('(')?;
548 self.expect(')')?;
549 Ok(GremlinStep::Next)
550 }
551 "fold" => {
552 self.expect('(')?;
553 self.expect(')')?;
554 Ok(GremlinStep::Fold)
555 }
556
557 _ => Err(self.error(&format!("Unknown step: {}", name))),
558 }
559 }
560
561 fn parse_inner_traversal(&mut self) -> Result<GremlinTraversal, GremlinError> {
563 self.skip_whitespace();
564
565 if self.input[self.pos..].starts_with("__") || self.input[self.pos..].starts_with("g.") {
567 return self.parse_traversal();
568 }
569
570 let mut steps = Vec::new();
572
573 loop {
574 self.skip_whitespace();
575
576 if self.is_at_end() || self.peek() == Some(')') {
577 break;
578 }
579
580 self.consume_if(".");
582
583 self.skip_whitespace();
584
585 if self.is_at_end() || self.peek() == Some(')') {
586 break;
587 }
588
589 let step = self.parse_step()?;
590 steps.push(step);
591 }
592
593 Ok(GremlinTraversal {
594 source: TraversalSource::Anonymous,
595 steps,
596 })
597 }
598
599 fn skip_whitespace(&mut self) {
602 while let Some(c) = self.peek() {
603 if c.is_whitespace() {
604 self.pos += 1;
605 } else {
606 break;
607 }
608 }
609 }
610
611 fn peek(&self) -> Option<char> {
612 self.input[self.pos..].chars().next()
613 }
614
615 fn is_at_end(&self) -> bool {
616 self.pos >= self.input.len()
617 }
618
619 fn consume_if(&mut self, s: &str) -> bool {
620 if self.input[self.pos..].starts_with(s) {
621 self.pos += s.len();
622 true
623 } else {
624 false
625 }
626 }
627
628 fn expect(&mut self, c: char) -> Result<(), GremlinError> {
629 self.skip_whitespace();
630 if self.peek() == Some(c) {
631 self.pos += 1;
632 Ok(())
633 } else {
634 Err(self.error(&format!("Expected '{}', found {:?}", c, self.peek())))
635 }
636 }
637
638 fn parse_identifier(&mut self) -> Result<String, GremlinError> {
639 self.skip_whitespace();
640
641 let start = self.pos;
642 while let Some(c) = self.peek() {
643 if c.is_alphanumeric() || c == '_' {
644 self.pos += 1;
645 } else {
646 break;
647 }
648 }
649
650 if self.pos == start {
651 Err(self.error("Expected identifier"))
652 } else {
653 Ok(self.input[start..self.pos].to_string())
654 }
655 }
656
657 fn parse_string(&mut self) -> Result<String, GremlinError> {
658 self.skip_whitespace();
659
660 let quote = self.peek();
661 if quote != Some('\'') && quote != Some('"') {
662 return Err(self.error("Expected string"));
663 }
664 self.pos += 1;
665
666 let start = self.pos;
667 while let Some(c) = self.peek() {
668 if Some(c) == quote {
669 let s = self.input[start..self.pos].to_string();
670 self.pos += 1;
671 return Ok(s);
672 }
673 if c == '\\' {
674 self.pos += 2; } else {
676 self.pos += 1;
677 }
678 }
679
680 Err(self.error("Unterminated string"))
681 }
682
683 fn parse_optional_string_arg(&mut self) -> Result<Option<String>, GremlinError> {
684 self.skip_whitespace();
685 if self.peek() == Some(')') {
686 Ok(None)
687 } else if self.peek() == Some('\'') || self.peek() == Some('"') {
688 Ok(Some(self.parse_string()?))
689 } else {
690 let start = self.pos;
692 while let Some(c) = self.peek() {
693 if c.is_alphanumeric() || c == '_' || c == ':' || c == '.' || c == '-' {
694 self.pos += 1;
695 } else {
696 break;
697 }
698 }
699 if self.pos > start {
700 Ok(Some(self.input[start..self.pos].to_string()))
701 } else {
702 Ok(None)
703 }
704 }
705 }
706
707 fn parse_string_list(&mut self) -> Result<Vec<String>, GremlinError> {
708 let mut result = Vec::new();
709
710 self.skip_whitespace();
711 if self.peek() == Some(')') {
712 return Ok(result);
713 }
714
715 loop {
716 self.skip_whitespace();
717 if self.peek() == Some(')') {
718 break;
719 }
720
721 result.push(self.parse_string()?);
722
723 self.skip_whitespace();
724 if !self.consume_if(",") {
725 break;
726 }
727 }
728
729 Ok(result)
730 }
731
732 fn parse_integer(&mut self) -> Result<i64, GremlinError> {
733 self.skip_whitespace();
734
735 let start = self.pos;
736 if self.peek() == Some('-') {
737 self.pos += 1;
738 }
739
740 while let Some(c) = self.peek() {
741 if c.is_ascii_digit() {
742 self.pos += 1;
743 } else {
744 break;
745 }
746 }
747
748 let s = &self.input[start..self.pos];
749 s.parse()
750 .map_err(|_| self.error(&format!("Invalid integer: {}", s)))
751 }
752
753 fn parse_value(&mut self) -> Result<GremlinValue, GremlinError> {
754 self.skip_whitespace();
755
756 if self.peek() == Some('\'') || self.peek() == Some('"') {
758 return Ok(GremlinValue::String(self.parse_string()?));
759 }
760
761 if self.consume_if("true") {
763 return Ok(GremlinValue::Boolean(true));
764 }
765 if self.consume_if("false") {
766 return Ok(GremlinValue::Boolean(false));
767 }
768
769 let start = self.pos;
771 if self.peek() == Some('-') {
772 self.pos += 1;
773 }
774 while let Some(c) = self.peek() {
775 if c.is_ascii_digit() || c == '.' {
776 self.pos += 1;
777 } else {
778 break;
779 }
780 }
781
782 let s = &self.input[start..self.pos];
783 if s.contains('.') {
784 let f: f64 = s
785 .parse()
786 .map_err(|_| self.error(&format!("Invalid float: {}", s)))?;
787 Ok(GremlinValue::Float(f))
788 } else {
789 let i: i64 = s
790 .parse()
791 .map_err(|_| self.error(&format!("Invalid integer: {}", s)))?;
792 Ok(GremlinValue::Integer(i))
793 }
794 }
795
796 fn error(&self, message: &str) -> GremlinError {
797 GremlinError {
798 message: message.to_string(),
799 position: self.pos,
800 }
801 }
802}
803
804impl GremlinTraversal {
805 pub fn to_query_expr(&self) -> QueryExpr {
807 let mut nodes = Vec::new();
809 let mut edges = Vec::new();
810 let mut filters = Vec::new();
811 let mut projections = Vec::new();
812
813 let mut current_alias = "n0".to_string();
814 let mut alias_counter = 0;
815
816 for step in &self.steps {
817 match step {
818 GremlinStep::V(id) => {
819 let mut node = NodePattern {
820 alias: current_alias.clone(),
821 node_label: None,
822 properties: Vec::new(),
823 };
824 if let Some(id) = id {
825 node.properties.push(PropertyFilter {
826 name: "id".to_string(),
827 op: CompareOp::Eq,
828 value: Value::text(id.clone()),
829 });
830 }
831 nodes.push(node);
832 }
833 GremlinStep::HasLabel(label) => {
834 if let Some(last) = nodes.last_mut() {
835 let lower = label.to_lowercase();
838 last.node_label = Some(match lower.as_str() {
839 "vuln" => "vulnerability".to_string(),
840 "tech" => "technology".to_string(),
841 "cert" => "certificate".to_string(),
842 _ => lower,
843 });
844 }
845 }
846 GremlinStep::Has(key, value) => {
847 let field_ref = FieldRef::NodeProperty {
848 alias: current_alias.clone(),
849 property: key.clone(),
850 };
851 let filter = if let Some(val) = value {
852 Filter::Compare {
853 field: field_ref,
854 op: CompareOp::Eq,
855 value: match val {
856 GremlinValue::String(s) => Value::text(s.clone()),
857 GremlinValue::Integer(i) => Value::Integer(*i),
858 GremlinValue::Float(f) => Value::Float(*f),
859 GremlinValue::Boolean(b) => Value::Boolean(*b),
860 GremlinValue::Predicate(_) => Value::Null, },
862 }
863 } else {
864 Filter::IsNotNull(field_ref)
865 };
866 filters.push(filter);
867 }
868 GremlinStep::Out(label) | GremlinStep::In(label) | GremlinStep::Both(label) => {
869 alias_counter += 1;
870 let new_alias = format!("n{}", alias_counter);
871
872 let direction = match step {
873 GremlinStep::Out(_) => EdgeDirection::Outgoing,
874 GremlinStep::In(_) => EdgeDirection::Incoming,
875 GremlinStep::Both(_) => EdgeDirection::Both,
876 _ => EdgeDirection::Outgoing,
877 };
878
879 let edge_label = label.as_ref().map(|l| {
882 let lower = l.to_lowercase();
883 match lower.as_str() {
884 "hasservice" => "has_service".to_string(),
885 "hasendpoint" => "has_endpoint".to_string(),
886 "usestech" => "uses_tech".to_string(),
887 "authaccess" => "auth_access".to_string(),
888 "affectedby" => "affected_by".to_string(),
889 "connectsto" | "connects" => "connects_to".to_string(),
890 "relatedto" => "related_to".to_string(),
891 "hasuser" => "has_user".to_string(),
892 "hascert" => "has_cert".to_string(),
893 _ => lower,
894 }
895 });
896
897 edges.push(EdgePattern {
898 alias: None,
899 from: current_alias.clone(),
900 to: new_alias.clone(),
901 edge_label,
902 direction,
903 min_hops: 1,
904 max_hops: 1,
905 });
906
907 nodes.push(NodePattern {
908 alias: new_alias.clone(),
909 node_label: None,
910 properties: Vec::new(),
911 });
912
913 current_alias = new_alias;
914 }
915 GremlinStep::Limit(_n) => {
916 }
919 GremlinStep::Values(keys) => {
920 for key in keys {
921 projections.push(Projection::from_field(FieldRef::NodeProperty {
922 alias: current_alias.clone(),
923 property: key.clone(),
924 }));
925 }
926 }
927 GremlinStep::Count => {
928 projections.push(Projection::Field(
930 FieldRef::NodeId {
931 alias: current_alias.clone(),
932 },
933 Some("count".to_string()),
934 ));
935 }
936 GremlinStep::As(label) => {
937 if let Some(last) = nodes.last_mut() {
938 last.alias = label.clone();
939 current_alias = label.clone();
940 }
941 }
942 _ => {}
943 }
944 }
945
946 if projections.is_empty() {
948 projections.push(Projection::from_field(FieldRef::NodeId {
949 alias: current_alias.clone(),
950 }));
951 }
952
953 let combined_filter = if filters.is_empty() {
955 None
956 } else {
957 let mut iter = filters.into_iter();
958 let first = iter.next().unwrap();
959 Some(iter.fold(first, |acc, f| Filter::And(Box::new(acc), Box::new(f))))
960 };
961
962 QueryExpr::Graph(GraphQuery {
963 alias: None,
964 pattern: GraphPattern { nodes, edges },
965 filter: combined_filter,
966 return_: projections,
967 })
968 }
969}
970
971#[cfg(test)]
972mod tests {
973 use super::*;
974
975 #[test]
976 fn test_parse_simple_v() {
977 let t = GremlinParser::parse("g.V()").unwrap();
978 assert_eq!(t.source, TraversalSource::Graph);
979 assert_eq!(t.steps.len(), 1);
980 assert!(matches!(t.steps[0], GremlinStep::V(None)));
981 }
982
983 #[test]
984 fn test_parse_v_with_id() {
985 let t = GremlinParser::parse("g.V('host:10.0.0.1')").unwrap();
986 assert!(matches!(&t.steps[0], GremlinStep::V(Some(id)) if id == "host:10.0.0.1"));
987 }
988
989 #[test]
990 fn test_parse_has_label() {
991 let t = GremlinParser::parse("g.V().hasLabel('host')").unwrap();
992 assert_eq!(t.steps.len(), 2);
993 assert!(matches!(&t.steps[1], GremlinStep::HasLabel(l) if l == "host"));
994 }
995
996 #[test]
997 fn test_parse_has_key_value() {
998 let t = GremlinParser::parse("g.V().has('name', 'alice')").unwrap();
999 assert!(matches!(
1000 &t.steps[1],
1001 GremlinStep::Has(k, Some(GremlinValue::String(v))) if k == "name" && v == "alice"
1002 ));
1003 }
1004
1005 #[test]
1006 fn test_parse_out() {
1007 let t = GremlinParser::parse("g.V().out('knows')").unwrap();
1008 assert!(matches!(&t.steps[1], GremlinStep::Out(Some(l)) if l == "knows"));
1009 }
1010
1011 #[test]
1012 fn test_parse_chain() {
1013 let t =
1014 GremlinParser::parse("g.V().hasLabel('host').out('connects').has('port', 22)").unwrap();
1015 assert_eq!(t.steps.len(), 4);
1016 }
1017
1018 #[test]
1019 fn test_parse_limit() {
1020 let t = GremlinParser::parse("g.V().limit(10)").unwrap();
1021 assert!(matches!(t.steps[1], GremlinStep::Limit(10)));
1022 }
1023
1024 #[test]
1025 fn test_parse_count() {
1026 let t = GremlinParser::parse("g.V().count()").unwrap();
1027 assert!(matches!(t.steps[1], GremlinStep::Count));
1028 }
1029
1030 #[test]
1031 fn test_parse_repeat_times() {
1032 let t = GremlinParser::parse("g.V().repeat(out()).times(3)").unwrap();
1033 assert_eq!(t.steps.len(), 3);
1034 assert!(matches!(&t.steps[1], GremlinStep::Repeat(_)));
1035 assert!(matches!(t.steps[2], GremlinStep::Times(3)));
1036 }
1037
1038 #[test]
1039 fn test_parse_anonymous() {
1040 let t = GremlinParser::parse("__.out('knows')").unwrap();
1041 assert_eq!(t.source, TraversalSource::Anonymous);
1042 assert!(matches!(&t.steps[0], GremlinStep::Out(Some(l)) if l == "knows"));
1043 }
1044
1045 #[test]
1046 fn test_parse_values() {
1047 let t = GremlinParser::parse("g.V().values('name', 'age')").unwrap();
1048 assert!(matches!(&t.steps[1], GremlinStep::Values(keys) if keys.len() == 2));
1049 }
1050
1051 #[test]
1052 fn test_to_query_expr() {
1053 let t = GremlinParser::parse("g.V().hasLabel('host').out('connects').limit(10)").unwrap();
1054 let expr = t.to_query_expr();
1055 assert!(matches!(expr, QueryExpr::Graph(_)));
1056 }
1057}