Skip to main content

ryo_query_language/
schema.rs

1//! RyoQL Schema - 型定義
2
3use ryo_pattern::{BodyMatch, Relations};
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6
7// =============================================================================
8// Query
9// =============================================================================
10
11/// RyoQLクエリ
12#[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    /// 何を探すか
20    pub kind: QueryKind,
21
22    /// どう絞り込むか
23    #[serde(default, skip_serializing_if = "Option::is_none")]
24    pub r#match: Option<MatchAttrs>,
25
26    /// ネスト条件
27    #[serde(default, skip_serializing_if = "Vec::is_empty")]
28    pub inner: Vec<Query>,
29
30    /// Or/And用: サブクエリ
31    #[serde(default, skip_serializing_if = "Vec::is_empty")]
32    pub queries: Vec<Query>,
33
34    /// Pattern用: パターン名
35    #[serde(default, skip_serializing_if = "Option::is_none")]
36    pub name: Option<String>,
37
38    /// Body pattern matching (RyoPattern extension)
39    ///
40    /// Match patterns within function/method bodies.
41    /// ```yaml
42    /// body:
43    ///   contains:
44    ///     - node: MethodCall
45    ///       method: { name: "unwrap" }
46    /// ```
47    #[serde(default, skip_serializing_if = "Option::is_none")]
48    pub body: Option<BodyMatch>,
49
50    /// Relation conditions with logical grouping (RyoPattern extension)
51    ///
52    /// Filter by graph relationships: `any`/`all`/`none`.
53    /// ```yaml
54    /// relations:
55    ///   any:
56    ///     - kind: Calls
57    ///       target: { kind: Function, match: { name: "unwrap" } }
58    ///   none:
59    ///     - kind: DependsOn
60    ///       target: { kind: Struct, match: { name: "Database" } }
61    /// ```
62    #[serde(default, skip_serializing_if = "Option::is_none")]
63    pub relations: Option<Relations>,
64
65    /// LSP連携(オプション)
66    #[serde(default, skip_serializing_if = "Option::is_none")]
67    pub resolve: Option<ResolveConfig>,
68
69    /// 検索範囲
70    #[serde(default, skip_serializing_if = "Option::is_none")]
71    pub scope: Option<Scope>,
72
73    /// 出力形式
74    #[serde(default, skip_serializing_if = "Option::is_none")]
75    pub view: Option<ViewMode>,
76
77    /// 件数制限
78    #[serde(default, skip_serializing_if = "Option::is_none")]
79    pub limit: Option<usize>,
80}
81
82// =============================================================================
83// QueryKind
84// =============================================================================
85
86/// クエリ対象の種類
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
88pub enum QueryKind {
89    /// 全種類を検索(初回探索用)
90    Any,
91
92    // 基本
93    /// Free functions のみ。impl内のメソッド (Method) は含まない。
94    /// Method単体をクエリする機能は未対応。
95    Function,
96    /// `struct` 定義。
97    Struct,
98    /// `enum` 定義。
99    Enum,
100    /// `trait` 定義。
101    Trait,
102    /// `impl` ブロック。
103    Impl,
104    /// `mod` 定義。
105    Mod,
106    /// `const` 定義。
107    Const,
108    /// `static` 定義。
109    Static,
110    /// `type` エイリアス定義。
111    TypeAlias,
112
113    // inner用
114    /// 戻り値型 (inner クエリ用)。
115    ReturnType,
116    /// 関数パラメータ (inner クエリ用)。
117    Parameter,
118    /// struct フィールド (inner クエリ用)。
119    Field,
120    /// enum バリアント (inner クエリ用)。
121    Variant,
122
123    // 複合
124    /// 子クエリの和集合。
125    Or,
126    /// 子クエリの積集合。
127    And,
128
129    // 事前定義パターン
130    /// 事前定義パターンによる検索。
131    Pattern,
132
133    /// リテラル検索(文字列、数値、真偽値等)
134    /// requires: literal-search feature in ryo-analysis
135    Literal,
136}
137
138// =============================================================================
139// MatchAttrs
140// =============================================================================
141
142/// マッチング条件
143#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
144pub struct MatchAttrs {
145    /// パターンマッチ (globパターンのショートハンド)
146    ///
147    /// `"*Config"` は `{"name": {"glob": "*Config"}}` と等価。
148    /// nameと同時に指定された場合、nameが優先される。
149    #[serde(default, skip_serializing_if = "Option::is_none")]
150    pub pattern: Option<String>,
151
152    /// 名前マッチ (詳細指定)
153    #[serde(default, skip_serializing_if = "Option::is_none")]
154    pub name: Option<NameMatcher>,
155
156    /// SymbolId直接指定
157    ///
158    /// discoverで出力される `SymbolId(165v1)` 形式、または `165v1` 形式で指定。
159    /// 指定された場合、他のマッチング条件より優先される(直接lookup)。
160    #[serde(default, skip_serializing_if = "Option::is_none")]
161    pub symbol_id: Option<String>,
162
163    /// Ignore ASCII case when matching (A-Z == a-z)
164    ///
165    /// Applies to `pattern` shorthand. For `name` detailed matching,
166    /// use `name.ignore_case` instead.
167    #[serde(default, skip_serializing_if = "Option::is_none")]
168    pub ignore_case: Option<bool>,
169
170    /// Ignore word separators and casing style (snake_case == camelCase == PascalCase)
171    ///
172    /// Applies to `pattern` shorthand. For `name` detailed matching,
173    /// use `name.ignore_word_separate` instead.
174    #[serde(default, skip_serializing_if = "Option::is_none")]
175    pub ignore_word_separate: Option<bool>,
176
177    /// 可視性
178    #[serde(default, skip_serializing_if = "Option::is_none")]
179    pub vis: Option<Visibility>,
180
181    /// async関数か
182    #[serde(default, skip_serializing_if = "Option::is_none")]
183    pub is_async: Option<bool>,
184
185    /// unsafe関数か
186    #[serde(default, skip_serializing_if = "Option::is_none")]
187    pub is_unsafe: Option<bool>,
188
189    /// selfレシーバー種別
190    #[serde(default, skip_serializing_if = "Option::is_none")]
191    pub receiver: Option<ReceiverKind>,
192
193    /// アトリビュート
194    #[serde(default, skip_serializing_if = "Option::is_none")]
195    pub attributes: Option<Vec<String>>,
196
197    /// ジェネリクス条件
198    #[serde(default, skip_serializing_if = "Option::is_none")]
199    pub generics: Option<GenericsMatch>,
200
201    /// 失敗時リカバリー
202    #[serde(default, skip_serializing_if = "Option::is_none")]
203    pub on_empty: Option<RecoveryStrategy>,
204
205    /// リテラル種別フィルタ(Literalクエリ用)
206    #[serde(default, skip_serializing_if = "Option::is_none")]
207    pub lit_type: Option<LiteralType>,
208
209    /// 親シンボルでフィルタ(Variant, Field, Method用)
210    ///
211    /// 例: `{"kind": "Variant", "match": {"parent": "Filter"}}` →
212    /// Filter enumのVariantのみを返す
213    #[serde(default, skip_serializing_if = "Option::is_none")]
214    pub parent: Option<NameMatcher>,
215}
216
217// =============================================================================
218// LiteralType
219// =============================================================================
220
221/// リテラルの種類(Literalクエリ用フィルタ)
222#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
223pub enum LiteralType {
224    /// 文字列リテラル: `"hello"`, `r#"raw"#`
225    String,
226    /// バイト文字列: `b"bytes"`
227    ByteStr,
228    /// 文字: `'a'`
229    Char,
230    /// バイト: `b'x'`
231    Byte,
232    /// 整数: `42`, `0xFF`
233    Int,
234    /// 浮動小数点: `3.14`
235    Float,
236    /// 真偽値: `true`, `false`
237    Bool,
238}
239
240// =============================================================================
241// NameMatcher
242// =============================================================================
243
244/// 名前マッチング方法
245#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
246#[serde(untagged)]
247pub enum NameMatcher {
248    /// 完全一致 (文字列直接指定)
249    Exact(String),
250    /// 詳細指定
251    Detailed(NameMatcherDetailed),
252}
253
254/// 詳細な名前マッチング
255#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
256pub struct NameMatcherDetailed {
257    /// 部分一致 (contains)。
258    #[serde(default, skip_serializing_if = "Option::is_none")]
259    pub contains: Option<String>,
260
261    /// 前方一致 (starts_with)。
262    #[serde(default, skip_serializing_if = "Option::is_none")]
263    pub starts_with: Option<String>,
264
265    /// 後方一致 (ends_with)。
266    #[serde(default, skip_serializing_if = "Option::is_none")]
267    pub ends_with: Option<String>,
268
269    /// 正規表現マッチ。
270    #[serde(default, skip_serializing_if = "Option::is_none")]
271    pub regex: Option<String>,
272
273    /// glob パターンマッチ (`*`/`?`)。
274    #[serde(default, skip_serializing_if = "Option::is_none")]
275    pub glob: Option<String>,
276
277    /// Ignore ASCII case when matching (A-Z == a-z)
278    #[serde(default, skip_serializing_if = "Option::is_none")]
279    pub ignore_case: Option<bool>,
280
281    /// Ignore word separators and casing style (snake_case == camelCase == PascalCase)
282    #[serde(default, skip_serializing_if = "Option::is_none")]
283    pub ignore_word_separate: Option<bool>,
284}
285
286impl NameMatcher {
287    /// 名前がマッチするか判定
288    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    /// Check if the name matches all specified criteria.
298    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        // For word-separate matching, normalize both pattern and name to words
303        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            // Regex with word-separate is not supported
331            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            // Case-insensitive matching
340            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            // Default: case-sensitive matching
376            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
410// =============================================================================
411// Word Normalization Utilities (duplicated from ryo-analysis for independence)
412// =============================================================================
413
414/// Normalize an identifier to lowercase words.
415fn 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(&current)
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(&current)
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(&current)
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(&current)
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// =============================================================================
590// ReceiverKind
591// =============================================================================
592
593/// selfレシーバーの種類
594#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
595pub enum ReceiverKind {
596    /// 関連関数 (fn new())
597    None,
598    /// &self
599    Ref,
600    /// &mut self
601    MutRef,
602    /// self
603    Owned,
604}
605
606// =============================================================================
607// Visibility
608// =============================================================================
609
610/// 可視性
611#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
612pub enum Visibility {
613    /// `pub`
614    Public,
615    /// 非公開 (modifier 無し)。
616    Private,
617    /// `pub(crate)`
618    Crate,
619    /// `pub(super)`
620    Super,
621    /// pub(in path)
622    Restricted(String),
623}
624
625// =============================================================================
626// GenericsMatch
627// =============================================================================
628
629/// ジェネリクスマッチング条件
630#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
631pub struct GenericsMatch {
632    /// 型パラメータ名 ["T", "E"]
633    #[serde(default, skip_serializing_if = "Option::is_none")]
634    pub params: Option<Vec<String>>,
635
636    /// 境界マッチ ["Clone", "Send"]
637    #[serde(default, skip_serializing_if = "Option::is_none")]
638    pub bounds: Option<Vec<NameMatcher>>,
639
640    /// ライフタイム ["'a"]
641    #[serde(default, skip_serializing_if = "Option::is_none")]
642    pub lifetimes: Option<Vec<String>>,
643}
644
645// =============================================================================
646// RecoveryStrategy
647// =============================================================================
648
649/// 失敗時リカバリー戦略
650#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
651pub struct RecoveryStrategy {
652    /// Fuzzy検索
653    #[serde(default, skip_serializing_if = "Option::is_none")]
654    pub fuzzy: Option<FuzzyConfig>,
655
656    /// 単語分割して検索
657    #[serde(default, skip_serializing_if = "Option::is_none")]
658    pub split_words: Option<bool>,
659
660    /// スコープ内を列挙
661    #[serde(default, skip_serializing_if = "Option::is_none")]
662    pub enumerate_scope: Option<usize>,
663}
664
665/// Fuzzy検索設定
666#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
667pub struct FuzzyConfig {
668    /// 許容する Levenshtein 距離の上限。
669    pub max_distance: u32,
670}
671
672// =============================================================================
673// ResolveConfig
674// =============================================================================
675
676/// LSP連携設定
677#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
678pub struct ResolveConfig {
679    /// 解決種別 (references / definition / callers ...)。
680    pub kind: ResolveKind,
681
682    /// LSP 呼び出しのタイムアウト (ミリ秒)。
683    #[serde(default, skip_serializing_if = "Option::is_none")]
684    pub timeout_ms: Option<u32>,
685
686    /// 解決を再帰する最大深度。
687    #[serde(default, skip_serializing_if = "Option::is_none")]
688    pub depth: Option<usize>,
689}
690
691/// Resolve種別
692#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
693pub enum ResolveKind {
694    /// LSP: 参照を列挙。
695    References,
696    /// LSP: 定義位置を取得。
697    Definition,
698    /// LSP: 呼び出し元 (callers) を列挙。
699    Callers,
700    /// LSP: 呼び出し先 (callees) を列挙。
701    Callees,
702    /// LSP: シンボル利用箇所を列挙。
703    Uses,
704    /// LSP: シンボルを利用している側を列挙。
705    UsedBy,
706    /// LSP: trait 実装を列挙。
707    Implementations,
708}
709
710// =============================================================================
711// Scope
712// =============================================================================
713
714/// 検索スコープ
715#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
716pub struct Scope {
717    /// パスパターン "src/**"
718    #[serde(default, skip_serializing_if = "Option::is_none")]
719    pub path: Option<String>,
720
721    /// 除外パターン "tests/**"
722    #[serde(default, skip_serializing_if = "Option::is_none")]
723    pub exclude_path: Option<String>,
724
725    /// モジュールパス
726    #[serde(default, skip_serializing_if = "Option::is_none")]
727    pub module: Option<String>,
728}
729
730// =============================================================================
731// ViewMode
732// =============================================================================
733
734/// 出力モード
735#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
736pub enum ViewMode {
737    /// コード片 + 周辺行(デフォルト)
738    #[default]
739    Snippet,
740    /// 位置のみ
741    Precise,
742    /// 件数のみ
743    Count,
744    /// 定義詳細(ModPath + Definition + SpecDoc)
745    Def,
746    /// 完全な定義(Def + 関数body)
747    Full,
748}
749
750// =============================================================================
751// Response
752// =============================================================================
753
754/// クエリレスポンス
755#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
756pub struct QueryResponse {
757    /// 検索全体のステータス (found / not_found / partial)。
758    pub status: QueryStatus,
759    /// マッチ結果一覧。
760    pub results: Vec<MatchResult>,
761    /// 結果ゼロ時等に返される候補サジェスチョン。
762    pub suggestions: Vec<Suggestion>,
763    /// 経過時間等のメタ情報。
764    pub metadata: QueryMetadata,
765}
766
767/// クエリステータス
768#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
769pub enum QueryStatus {
770    /// 1 件以上ヒット。
771    Found,
772    /// ヒットなし。
773    NotFound,
774    /// 部分的にヒット (resolve がタイムアウト等)。
775    Partial,
776}
777
778/// マッチ結果
779///
780/// # 設計方針
781///
782/// - **SymbolId が主キー**: これだけで処理に十分
783/// - **SymbolPath**: フルパスでシンボルを特定可能
784/// - **file_path/line は含めない**: Spanを保持しない方針。ModPathで大体わかる
785/// - **ファイル参照が必要な場合**: Grep等で対応可能
786/// - **ViewMode別データ**: MatchViewで型安全に表現
787///
788/// この設計により、結果がコンパクトになりAIエージェントが扱いやすくなる。
789#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
790pub struct MatchResult {
791    /// SymbolId: slotmap key (e.g., "SymbolId(172v1)")
792    ///
793    /// **Warning**: This ID is session-volatile. It changes when the server restarts.
794    /// For persistent references across sessions, use `uuid` instead.
795    pub id: String,
796
797    /// Persistent UUID for cross-session symbol tracking.
798    ///
799    /// This UUID survives server restarts and symbol renames.
800    /// Use this for storing references that need to persist.
801    /// Returns `None` if the symbol hasn't been assigned a persistent ID.
802    #[serde(default, skip_serializing_if = "Option::is_none")]
803    pub uuid: Option<String>,
804
805    /// SymbolPath: Full path (e.g., "ryo_analysis::registry::DetectRegistry")
806    pub path: String,
807
808    /// Item kind (Function, Struct, Enum, etc.)
809    pub node_kind: String,
810
811    /// Symbol name (e.g., "Node")
812    pub name: String,
813
814    /// ViewMode別のデータ
815    #[serde(flatten)]
816    pub view: MatchView,
817}
818
819/// ViewMode別のマッチデータ
820///
821/// 各ViewModeで必要なデータのみを持つ。
822/// CountモードではMatchResult自体が不要なので含めない。
823#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
824#[serde(tag = "view_mode", rename_all = "snake_case")]
825pub enum MatchView {
826    /// Snippetモード: コード片
827    Snippet {
828        /// コードスニペット
829        text: String,
830    },
831
832    /// Preciseモード: 位置情報のみ(追加データなし)
833    Precise,
834
835    /// Defモード: 定義詳細(シグネチャ + ドキュメント)
836    Def {
837        /// モジュールパス (e.g., "ryo_core::ast")
838        module_path: String,
839        /// 定義(シグネチャ、フィールド等)
840        definition: String,
841        /// ドキュメント + spec annotations
842        #[serde(default, skip_serializing_if = "Option::is_none")]
843        doc: Option<String>,
844    },
845
846    /// Fullモード: 完全な定義(Def + 関数body)
847    Full {
848        /// モジュールパス
849        module_path: String,
850        /// 定義シグネチャ
851        definition: String,
852        /// 関数body等の完全なソース
853        body: String,
854        /// ドキュメント + spec annotations
855        #[serde(default, skip_serializing_if = "Option::is_none")]
856        doc: Option<String>,
857    },
858}
859
860/// サジェスチョン
861#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
862pub struct Suggestion {
863    /// 候補の種別 (typo / similar / in_scope)。
864    pub kind: SuggestionKind,
865    /// 候補シンボル名。
866    pub name: String,
867
868    /// 元クエリとの編集距離 (typo 候補のみ)。
869    #[serde(default, skip_serializing_if = "Option::is_none")]
870    pub distance: Option<u32>,
871
872    /// 候補の確からしさ (0.0-1.0)。
873    pub confidence: f32,
874}
875
876/// サジェスチョン種別
877#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
878pub enum SuggestionKind {
879    /// タイポ候補 (編集距離が近い)。
880    Typo,
881    /// 類似名候補。
882    Similar,
883    /// 同スコープ内に存在する候補。
884    InScope,
885}
886
887/// クエリメタデータ
888#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
889pub struct QueryMetadata {
890    /// 検索全体の経過時間 (ミリ秒)。
891    pub elapsed_ms: u32,
892    /// `limit` 適用前の総マッチ数。
893    pub total_matches: usize,
894
895    /// resolve (LSP 連携) のステータス。
896    #[serde(default, skip_serializing_if = "Option::is_none")]
897    pub resolve_status: Option<ResolveStatus>,
898}
899
900/// Resolveステータス
901#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
902pub enum ResolveStatus {
903    /// すべての resolve が完了した。
904    Complete,
905    /// タイムアウトで一部 resolve 未完了。
906    TimedOut,
907    /// レートリミットで resolve 抑制された。
908    RateLimited,
909}
910
911// =============================================================================
912// Schema Generation
913// =============================================================================
914
915/// Generate JSON Schema for RyoQL Query
916pub fn query_json_schema() -> schemars::Schema {
917    schemars::schema_for!(Query)
918}
919
920/// Generate JSON Schema for RyoQL QueryResponse
921pub fn response_json_schema() -> schemars::Schema {
922    schemars::schema_for!(QueryResponse)
923}
924
925/// Generate JSON Schema as pretty-printed string
926pub fn query_json_schema_string() -> String {
927    serde_json::to_string_pretty(&query_json_schema()).unwrap_or_default()
928}
929
930/// Generate JSON Schema for response as pretty-printed string
931pub fn response_json_schema_string() -> String {
932    serde_json::to_string_pretty(&response_json_schema()).unwrap_or_default()
933}
934
935// =============================================================================
936// Tests
937// =============================================================================
938
939#[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        // BUG#1 regression: is_async at top level was silently ignored
1137        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        // Correct format: is_async nested inside match
1148        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        // any: Calls with transitive
1176        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        // none: TypeReferences
1183        let none = relations.none.unwrap();
1184        assert_eq!(none.len(), 1);
1185        assert_eq!(none[0].kind, RelationKind::TypeReferences);
1186    }
1187}