1use ryo_pattern::{BodyMatch, Relations};
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
13#[serde(deny_unknown_fields)]
14#[schemars(
15 title = "RyoQL Query",
16 description = "AI agent-friendly structured code query"
17)]
18pub struct Query {
19 pub kind: QueryKind,
21
22 #[serde(default, skip_serializing_if = "Option::is_none")]
24 pub r#match: Option<MatchAttrs>,
25
26 #[serde(default, skip_serializing_if = "Vec::is_empty")]
28 pub inner: Vec<Query>,
29
30 #[serde(default, skip_serializing_if = "Vec::is_empty")]
32 pub queries: Vec<Query>,
33
34 #[serde(default, skip_serializing_if = "Option::is_none")]
36 pub name: Option<String>,
37
38 #[serde(default, skip_serializing_if = "Option::is_none")]
48 pub body: Option<BodyMatch>,
49
50 #[serde(default, skip_serializing_if = "Option::is_none")]
63 pub relations: Option<Relations>,
64
65 #[serde(default, skip_serializing_if = "Option::is_none")]
67 pub resolve: Option<ResolveConfig>,
68
69 #[serde(default, skip_serializing_if = "Option::is_none")]
71 pub scope: Option<Scope>,
72
73 #[serde(default, skip_serializing_if = "Option::is_none")]
75 pub view: Option<ViewMode>,
76
77 #[serde(default, skip_serializing_if = "Option::is_none")]
79 pub limit: Option<usize>,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
88pub enum QueryKind {
89 Any,
91
92 Function,
96 Struct,
98 Enum,
100 Trait,
102 Impl,
104 Mod,
106 Const,
108 Static,
110 TypeAlias,
112
113 ReturnType,
116 Parameter,
118 Field,
120 Variant,
122
123 Or,
126 And,
128
129 Pattern,
132
133 Literal,
136}
137
138#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
144pub struct MatchAttrs {
145 #[serde(default, skip_serializing_if = "Option::is_none")]
150 pub pattern: Option<String>,
151
152 #[serde(default, skip_serializing_if = "Option::is_none")]
154 pub name: Option<NameMatcher>,
155
156 #[serde(default, skip_serializing_if = "Option::is_none")]
161 pub symbol_id: Option<String>,
162
163 #[serde(default, skip_serializing_if = "Option::is_none")]
168 pub ignore_case: Option<bool>,
169
170 #[serde(default, skip_serializing_if = "Option::is_none")]
175 pub ignore_word_separate: Option<bool>,
176
177 #[serde(default, skip_serializing_if = "Option::is_none")]
179 pub vis: Option<Visibility>,
180
181 #[serde(default, skip_serializing_if = "Option::is_none")]
183 pub is_async: Option<bool>,
184
185 #[serde(default, skip_serializing_if = "Option::is_none")]
187 pub is_unsafe: Option<bool>,
188
189 #[serde(default, skip_serializing_if = "Option::is_none")]
191 pub receiver: Option<ReceiverKind>,
192
193 #[serde(default, skip_serializing_if = "Option::is_none")]
195 pub attributes: Option<Vec<String>>,
196
197 #[serde(default, skip_serializing_if = "Option::is_none")]
199 pub generics: Option<GenericsMatch>,
200
201 #[serde(default, skip_serializing_if = "Option::is_none")]
203 pub on_empty: Option<RecoveryStrategy>,
204
205 #[serde(default, skip_serializing_if = "Option::is_none")]
207 pub lit_type: Option<LiteralType>,
208
209 #[serde(default, skip_serializing_if = "Option::is_none")]
214 pub parent: Option<NameMatcher>,
215}
216
217#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
223pub enum LiteralType {
224 String,
226 ByteStr,
228 Char,
230 Byte,
232 Int,
234 Float,
236 Bool,
238}
239
240#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
246#[serde(untagged)]
247pub enum NameMatcher {
248 Exact(String),
250 Detailed(NameMatcherDetailed),
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
256pub struct NameMatcherDetailed {
257 #[serde(default, skip_serializing_if = "Option::is_none")]
259 pub contains: Option<String>,
260
261 #[serde(default, skip_serializing_if = "Option::is_none")]
263 pub starts_with: Option<String>,
264
265 #[serde(default, skip_serializing_if = "Option::is_none")]
267 pub ends_with: Option<String>,
268
269 #[serde(default, skip_serializing_if = "Option::is_none")]
271 pub regex: Option<String>,
272
273 #[serde(default, skip_serializing_if = "Option::is_none")]
275 pub glob: Option<String>,
276
277 #[serde(default, skip_serializing_if = "Option::is_none")]
279 pub ignore_case: Option<bool>,
280
281 #[serde(default, skip_serializing_if = "Option::is_none")]
283 pub ignore_word_separate: Option<bool>,
284}
285
286impl NameMatcher {
287 pub fn matches(&self, name: &str) -> bool {
289 match self {
290 NameMatcher::Exact(pattern) => name == pattern,
291 NameMatcher::Detailed(d) => d.matches(name),
292 }
293 }
294}
295
296impl NameMatcherDetailed {
297 pub fn matches(&self, name: &str) -> bool {
299 let ignore_case = self.ignore_case.unwrap_or(false);
300 let ignore_word_separate = self.ignore_word_separate.unwrap_or(false);
301
302 if ignore_word_separate {
304 let name_words = normalize_to_words(name);
305
306 if let Some(ref contains) = self.contains {
307 let pattern_words = normalize_to_words(contains);
308 if !contains_words(&name_words, &pattern_words) {
309 return false;
310 }
311 }
312 if let Some(ref starts) = self.starts_with {
313 let pattern_words = normalize_to_words(starts);
314 if !starts_with_words(&name_words, &pattern_words) {
315 return false;
316 }
317 }
318 if let Some(ref ends) = self.ends_with {
319 let pattern_words = normalize_to_words(ends);
320 if !ends_with_words(&name_words, &pattern_words) {
321 return false;
322 }
323 }
324 if let Some(ref pattern) = self.glob {
325 let pattern_words = normalize_pattern_to_words(pattern);
326 if !match_word_pattern(&pattern_words, &name_words) {
327 return false;
328 }
329 }
330 if let Some(ref pattern) = self.regex {
332 if let Ok(re) = regex::Regex::new(pattern) {
333 if !re.is_match(name) {
334 return false;
335 }
336 }
337 }
338 } else if ignore_case {
339 let name_lower = name.to_ascii_lowercase();
341
342 if let Some(ref contains) = self.contains {
343 if !name_lower.contains(&contains.to_ascii_lowercase()) {
344 return false;
345 }
346 }
347 if let Some(ref starts) = self.starts_with {
348 if !name_lower.starts_with(&starts.to_ascii_lowercase()) {
349 return false;
350 }
351 }
352 if let Some(ref ends) = self.ends_with {
353 if !name_lower.ends_with(&ends.to_ascii_lowercase()) {
354 return false;
355 }
356 }
357 if let Some(ref pattern) = self.regex {
358 if let Ok(re) = regex::RegexBuilder::new(pattern)
359 .case_insensitive(true)
360 .build()
361 {
362 if !re.is_match(name) {
363 return false;
364 }
365 }
366 }
367 if let Some(ref pattern) = self.glob {
368 if let Ok(glob_pattern) = glob::Pattern::new(&pattern.to_ascii_lowercase()) {
369 if !glob_pattern.matches(&name_lower) {
370 return false;
371 }
372 }
373 }
374 } else {
375 if let Some(ref contains) = self.contains {
377 if !name.contains(contains) {
378 return false;
379 }
380 }
381 if let Some(ref starts) = self.starts_with {
382 if !name.starts_with(starts) {
383 return false;
384 }
385 }
386 if let Some(ref ends) = self.ends_with {
387 if !name.ends_with(ends) {
388 return false;
389 }
390 }
391 if let Some(ref pattern) = self.regex {
392 if let Ok(re) = regex::Regex::new(pattern) {
393 if !re.is_match(name) {
394 return false;
395 }
396 }
397 }
398 if let Some(ref pattern) = self.glob {
399 if let Ok(glob_pattern) = glob::Pattern::new(pattern) {
400 if !glob_pattern.matches(name) {
401 return false;
402 }
403 }
404 }
405 }
406 true
407 }
408}
409
410fn normalize_to_words(s: &str) -> Vec<String> {
416 let mut words = Vec::new();
417 let mut current_word = String::new();
418
419 let chars: Vec<char> = s.chars().collect();
420 let len = chars.len();
421
422 for i in 0..len {
423 let c = chars[i];
424
425 if c == '_' {
426 if !current_word.is_empty() {
427 words.push(current_word.to_ascii_lowercase());
428 current_word.clear();
429 }
430 } else if c.is_ascii_uppercase() {
431 let prev_lower = i > 0 && chars[i - 1].is_ascii_lowercase();
432 let next_lower = i + 1 < len && chars[i + 1].is_ascii_lowercase();
433
434 if (prev_lower || (i > 0 && !current_word.is_empty() && next_lower))
435 && !current_word.is_empty()
436 {
437 words.push(current_word.to_ascii_lowercase());
438 current_word.clear();
439 }
440 current_word.push(c);
441 } else {
442 current_word.push(c);
443 }
444 }
445
446 if !current_word.is_empty() {
447 words.push(current_word.to_ascii_lowercase());
448 }
449
450 words
451}
452
453#[derive(Debug, Clone, PartialEq, Eq)]
454enum PatternWord {
455 Literal(String),
456 AnyWords,
457 AnyChar,
458}
459
460fn normalize_pattern_to_words(pattern: &str) -> Vec<PatternWord> {
461 let mut result = Vec::new();
462 let mut current = String::new();
463 let mut in_wildcard_seq = false;
464
465 for c in pattern.chars() {
466 match c {
467 '*' => {
468 if !current.is_empty() {
469 result.extend(
470 normalize_to_words(¤t)
471 .into_iter()
472 .map(PatternWord::Literal),
473 );
474 current.clear();
475 }
476 if !in_wildcard_seq {
477 result.push(PatternWord::AnyWords);
478 in_wildcard_seq = true;
479 }
480 }
481 '?' => {
482 if !current.is_empty() {
483 result.extend(
484 normalize_to_words(¤t)
485 .into_iter()
486 .map(PatternWord::Literal),
487 );
488 current.clear();
489 }
490 result.push(PatternWord::AnyChar);
491 in_wildcard_seq = false;
492 }
493 '_' => {
494 if !current.is_empty() {
495 result.extend(
496 normalize_to_words(¤t)
497 .into_iter()
498 .map(PatternWord::Literal),
499 );
500 current.clear();
501 }
502 in_wildcard_seq = false;
503 }
504 _ => {
505 current.push(c);
506 in_wildcard_seq = false;
507 }
508 }
509 }
510
511 if !current.is_empty() {
512 result.extend(
513 normalize_to_words(¤t)
514 .into_iter()
515 .map(PatternWord::Literal),
516 );
517 }
518
519 result
520}
521
522fn match_word_pattern(pattern: &[PatternWord], target: &[String]) -> bool {
523 match_word_pattern_recursive(pattern, target, 0, 0)
524}
525
526fn match_word_pattern_recursive(
527 pattern: &[PatternWord],
528 target: &[String],
529 pi: usize,
530 ti: usize,
531) -> bool {
532 if pi == pattern.len() && ti == target.len() {
533 return true;
534 }
535 if pi == pattern.len() {
536 return false;
537 }
538
539 match &pattern[pi] {
540 PatternWord::AnyWords => {
541 for skip in 0..=(target.len() - ti) {
542 if match_word_pattern_recursive(pattern, target, pi + 1, ti + skip) {
543 return true;
544 }
545 }
546 false
547 }
548 PatternWord::Literal(word) => {
549 if ti < target.len() && target[ti] == *word {
550 match_word_pattern_recursive(pattern, target, pi + 1, ti + 1)
551 } else {
552 false
553 }
554 }
555 PatternWord::AnyChar => {
556 if ti < target.len() {
557 match_word_pattern_recursive(pattern, target, pi + 1, ti + 1)
558 } else {
559 false
560 }
561 }
562 }
563}
564
565fn contains_words(haystack: &[String], needle: &[String]) -> bool {
566 if needle.is_empty() {
567 return true;
568 }
569 if needle.len() > haystack.len() {
570 return false;
571 }
572 haystack.windows(needle.len()).any(|w| w == needle)
573}
574
575fn starts_with_words(haystack: &[String], prefix: &[String]) -> bool {
576 if prefix.len() > haystack.len() {
577 return false;
578 }
579 haystack[..prefix.len()] == *prefix
580}
581
582fn ends_with_words(haystack: &[String], suffix: &[String]) -> bool {
583 if suffix.len() > haystack.len() {
584 return false;
585 }
586 haystack[haystack.len() - suffix.len()..] == *suffix
587}
588
589#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
595pub enum ReceiverKind {
596 None,
598 Ref,
600 MutRef,
602 Owned,
604}
605
606#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
612pub enum Visibility {
613 Public,
615 Private,
617 Crate,
619 Super,
621 Restricted(String),
623}
624
625#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
631pub struct GenericsMatch {
632 #[serde(default, skip_serializing_if = "Option::is_none")]
634 pub params: Option<Vec<String>>,
635
636 #[serde(default, skip_serializing_if = "Option::is_none")]
638 pub bounds: Option<Vec<NameMatcher>>,
639
640 #[serde(default, skip_serializing_if = "Option::is_none")]
642 pub lifetimes: Option<Vec<String>>,
643}
644
645#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
651pub struct RecoveryStrategy {
652 #[serde(default, skip_serializing_if = "Option::is_none")]
654 pub fuzzy: Option<FuzzyConfig>,
655
656 #[serde(default, skip_serializing_if = "Option::is_none")]
658 pub split_words: Option<bool>,
659
660 #[serde(default, skip_serializing_if = "Option::is_none")]
662 pub enumerate_scope: Option<usize>,
663}
664
665#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
667pub struct FuzzyConfig {
668 pub max_distance: u32,
670}
671
672#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
678pub struct ResolveConfig {
679 pub kind: ResolveKind,
681
682 #[serde(default, skip_serializing_if = "Option::is_none")]
684 pub timeout_ms: Option<u32>,
685
686 #[serde(default, skip_serializing_if = "Option::is_none")]
688 pub depth: Option<usize>,
689}
690
691#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
693pub enum ResolveKind {
694 References,
696 Definition,
698 Callers,
700 Callees,
702 Uses,
704 UsedBy,
706 Implementations,
708}
709
710#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
716pub struct Scope {
717 #[serde(default, skip_serializing_if = "Option::is_none")]
719 pub path: Option<String>,
720
721 #[serde(default, skip_serializing_if = "Option::is_none")]
723 pub exclude_path: Option<String>,
724
725 #[serde(default, skip_serializing_if = "Option::is_none")]
727 pub module: Option<String>,
728}
729
730#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
736pub enum ViewMode {
737 #[default]
739 Snippet,
740 Precise,
742 Count,
744 Def,
746 Full,
748}
749
750#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
756pub struct QueryResponse {
757 pub status: QueryStatus,
759 pub results: Vec<MatchResult>,
761 pub suggestions: Vec<Suggestion>,
763 pub metadata: QueryMetadata,
765}
766
767#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
769pub enum QueryStatus {
770 Found,
772 NotFound,
774 Partial,
776}
777
778#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
790pub struct MatchResult {
791 pub id: String,
796
797 #[serde(default, skip_serializing_if = "Option::is_none")]
803 pub uuid: Option<String>,
804
805 pub path: String,
807
808 pub node_kind: String,
810
811 pub name: String,
813
814 #[serde(flatten)]
816 pub view: MatchView,
817}
818
819#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
824#[serde(tag = "view_mode", rename_all = "snake_case")]
825pub enum MatchView {
826 Snippet {
828 text: String,
830 },
831
832 Precise,
834
835 Def {
837 module_path: String,
839 definition: String,
841 #[serde(default, skip_serializing_if = "Option::is_none")]
843 doc: Option<String>,
844 },
845
846 Full {
848 module_path: String,
850 definition: String,
852 body: String,
854 #[serde(default, skip_serializing_if = "Option::is_none")]
856 doc: Option<String>,
857 },
858}
859
860#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
862pub struct Suggestion {
863 pub kind: SuggestionKind,
865 pub name: String,
867
868 #[serde(default, skip_serializing_if = "Option::is_none")]
870 pub distance: Option<u32>,
871
872 pub confidence: f32,
874}
875
876#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
878pub enum SuggestionKind {
879 Typo,
881 Similar,
883 InScope,
885}
886
887#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
889pub struct QueryMetadata {
890 pub elapsed_ms: u32,
892 pub total_matches: usize,
894
895 #[serde(default, skip_serializing_if = "Option::is_none")]
897 pub resolve_status: Option<ResolveStatus>,
898}
899
900#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
902pub enum ResolveStatus {
903 Complete,
905 TimedOut,
907 RateLimited,
909}
910
911pub fn query_json_schema() -> schemars::Schema {
917 schemars::schema_for!(Query)
918}
919
920pub fn response_json_schema() -> schemars::Schema {
922 schemars::schema_for!(QueryResponse)
923}
924
925pub fn query_json_schema_string() -> String {
927 serde_json::to_string_pretty(&query_json_schema()).unwrap_or_default()
928}
929
930pub fn response_json_schema_string() -> String {
932 serde_json::to_string_pretty(&response_json_schema()).unwrap_or_default()
933}
934
935#[cfg(test)]
940mod tests {
941 use super::*;
942
943 #[test]
944 fn test_parse_simple_query() {
945 let yaml = r#"
946kind: Function
947match:
948 name: "process"
949 vis: Public
950 is_async: true
951"#;
952 let query: Query = serde_yaml::from_str(yaml).unwrap();
953 assert_eq!(query.kind, QueryKind::Function);
954 let m = query.r#match.unwrap();
955 assert!(matches!(m.name, Some(NameMatcher::Exact(ref s)) if s == "process"));
956 assert_eq!(m.vis, Some(Visibility::Public));
957 assert_eq!(m.is_async, Some(true));
958 }
959
960 #[test]
961 fn test_parse_nested_query() {
962 let yaml = r#"
963kind: Function
964match:
965 name: { starts_with: "process_" }
966inner:
967 - kind: ReturnType
968 match:
969 name: "Result"
970"#;
971 let query: Query = serde_yaml::from_str(yaml).unwrap();
972 assert_eq!(query.kind, QueryKind::Function);
973 assert_eq!(query.inner.len(), 1);
974 assert_eq!(query.inner[0].kind, QueryKind::ReturnType);
975 }
976
977 #[test]
978 fn test_parse_or_query() {
979 let yaml = r#"
980kind: Or
981queries:
982 - kind: Struct
983 match:
984 name: { contains: "Error" }
985 - kind: Enum
986 match:
987 name: { contains: "Error" }
988"#;
989 let query: Query = serde_yaml::from_str(yaml).unwrap();
990 assert_eq!(query.kind, QueryKind::Or);
991 assert_eq!(query.queries.len(), 2);
992 }
993
994 #[test]
995 fn test_name_matcher_exact() {
996 let matcher = NameMatcher::Exact("process".to_string());
997 assert!(matcher.matches("process"));
998 assert!(!matcher.matches("process_event"));
999 }
1000
1001 #[test]
1002 fn test_name_matcher_contains() {
1003 let matcher = NameMatcher::Detailed(NameMatcherDetailed {
1004 contains: Some("process".to_string()),
1005 starts_with: None,
1006 ends_with: None,
1007 regex: None,
1008 glob: None,
1009 ignore_case: None,
1010 ignore_word_separate: None,
1011 });
1012 assert!(matcher.matches("process"));
1013 assert!(matcher.matches("process_event"));
1014 assert!(matcher.matches("do_process"));
1015 assert!(!matcher.matches("handle"));
1016 }
1017
1018 #[test]
1019 fn test_name_matcher_glob() {
1020 let matcher = NameMatcher::Detailed(NameMatcherDetailed {
1021 contains: None,
1022 starts_with: None,
1023 ends_with: None,
1024 regex: None,
1025 glob: Some("*Config".to_string()),
1026 ignore_case: None,
1027 ignore_word_separate: None,
1028 });
1029 assert!(matcher.matches("AppConfig"));
1030 assert!(matcher.matches("Config"));
1031 assert!(!matcher.matches("ConfigManager"));
1032 }
1033
1034 #[test]
1035 fn test_name_matcher_ignore_case() {
1036 let matcher = NameMatcher::Detailed(NameMatcherDetailed {
1037 contains: Some("config".to_string()),
1038 starts_with: None,
1039 ends_with: None,
1040 regex: None,
1041 glob: None,
1042 ignore_case: Some(true),
1043 ignore_word_separate: None,
1044 });
1045 assert!(matcher.matches("AppConfig"));
1046 assert!(matcher.matches("APPCONFIG"));
1047 assert!(matcher.matches("config"));
1048 }
1049
1050 #[test]
1051 fn test_name_matcher_ignore_word_separate() {
1052 let matcher = NameMatcher::Detailed(NameMatcherDetailed {
1053 contains: None,
1054 starts_with: Some("get_user".to_string()),
1055 ends_with: None,
1056 regex: None,
1057 glob: None,
1058 ignore_case: None,
1059 ignore_word_separate: Some(true),
1060 });
1061 assert!(matcher.matches("get_user_name"));
1062 assert!(matcher.matches("getUserName"));
1063 assert!(matcher.matches("GetUserName"));
1064 assert!(!matcher.matches("fetch_user_name"));
1065 }
1066
1067 #[test]
1068 fn test_parse_literal_query() {
1069 let yaml = r#"
1070kind: Literal
1071match:
1072 pattern: "*error*"
1073 lit_type: String
1074"#;
1075 let query: Query = serde_yaml::from_str(yaml).unwrap();
1076 assert_eq!(query.kind, QueryKind::Literal);
1077 let m = query.r#match.unwrap();
1078 assert_eq!(m.pattern, Some("*error*".to_string()));
1079 assert_eq!(m.lit_type, Some(LiteralType::String));
1080 }
1081
1082 #[test]
1083 fn test_parse_literal_query_int() {
1084 let yaml = r#"
1085kind: Literal
1086match:
1087 pattern: "0x*"
1088 lit_type: Int
1089"#;
1090 let query: Query = serde_yaml::from_str(yaml).unwrap();
1091 assert_eq!(query.kind, QueryKind::Literal);
1092 let m = query.r#match.unwrap();
1093 assert_eq!(m.lit_type, Some(LiteralType::Int));
1094 }
1095
1096 #[test]
1097 fn test_literal_type_all_variants() {
1098 let types = [
1099 ("String", LiteralType::String),
1100 ("ByteStr", LiteralType::ByteStr),
1101 ("Char", LiteralType::Char),
1102 ("Byte", LiteralType::Byte),
1103 ("Int", LiteralType::Int),
1104 ("Float", LiteralType::Float),
1105 ("Bool", LiteralType::Bool),
1106 ];
1107 for (s, expected) in types {
1108 let json = format!(r#""{s}""#);
1109 let parsed: LiteralType = serde_json::from_str(&json).unwrap();
1110 assert_eq!(parsed, expected, "Failed for {s}");
1111 }
1112 }
1113
1114 #[test]
1115 fn test_parse_query_with_body() {
1116 let yaml = r#"
1117kind: Function
1118match:
1119 name: "process"
1120body:
1121 contains:
1122 - node: MethodCall
1123 capture: "call"
1124"#;
1125 let query: Query = serde_yaml::from_str(yaml).unwrap();
1126 assert_eq!(query.kind, QueryKind::Function);
1127 let body = query.body.unwrap();
1128 let contains = body.contains.unwrap();
1129 assert_eq!(contains.len(), 1);
1130 assert_eq!(contains[0].node, ryo_pattern::NodeKind::MethodCall);
1131 assert_eq!(contains[0].capture, Some("call".to_string()));
1132 }
1133
1134 #[test]
1135 fn test_deny_unknown_fields_rejects_top_level_is_async() {
1136 let json = r#"{"kind":"Function","is_async":true}"#;
1138 let result: Result<Query, _> = serde_json::from_str(json);
1139 assert!(
1140 result.is_err(),
1141 "top-level is_async must be rejected by deny_unknown_fields"
1142 );
1143 }
1144
1145 #[test]
1146 fn test_is_async_in_match_accepted() {
1147 let json = r#"{"kind":"Function","match":{"is_async":true}}"#;
1149 let query: Query = serde_json::from_str(json).unwrap();
1150 assert_eq!(query.r#match.unwrap().is_async, Some(true));
1151 }
1152
1153 #[test]
1154 fn test_parse_query_with_relations() {
1155 use ryo_pattern::RelationKind;
1156
1157 let yaml = r#"
1158kind: Function
1159match:
1160 name: "handler"
1161relations:
1162 any:
1163 - kind: Calls
1164 target: {}
1165 transitive: true
1166 max_depth: 3
1167 none:
1168 - kind: TypeReferences
1169 target: {}
1170"#;
1171 let query: Query = serde_yaml::from_str(yaml).unwrap();
1172 assert_eq!(query.kind, QueryKind::Function);
1173 let relations = query.relations.unwrap();
1174
1175 let any = relations.any.unwrap();
1177 assert_eq!(any.len(), 1);
1178 assert_eq!(any[0].kind, RelationKind::Calls);
1179 assert!(any[0].transitive);
1180 assert_eq!(any[0].max_depth, Some(3));
1181
1182 let none = relations.none.unwrap();
1184 assert_eq!(none.len(), 1);
1185 assert_eq!(none[0].kind, RelationKind::TypeReferences);
1186 }
1187}