ryo-query-language 0.1.0

RyoQL - Structured code query language for AI agents
Documentation
//! RyoQL Parser - YAML/JSON/CLI パース

use crate::schema::{MatchAttrs, NameMatcher, Query, QueryKind, ViewMode};
use thiserror::Error;

/// パースエラー
#[derive(Debug, Error)]
pub enum ParseError {
    /// YAML 構文エラー。
    #[error("YAML parse error: {0}")]
    Yaml(#[from] serde_yaml::Error),

    /// JSON 構文エラー。
    #[error("JSON parse error: {0}")]
    Json(#[from] serde_json::Error),

    /// 構造的に invalid な query (validate 失敗)。
    #[error("Invalid query: {0}")]
    Invalid(String),

    /// 未知の `QueryKind` 文字列。
    #[error("Unknown kind: {0}")]
    UnknownKind(String),
}

/// クエリパーサー
pub struct QueryParser;

impl QueryParser {
    /// YAMLからパース
    pub fn from_yaml(yaml: &str) -> Result<Query, ParseError> {
        let query: Query = serde_yaml::from_str(yaml)?;
        Self::validate(&query)?;
        Ok(query)
    }

    /// JSONからパース
    pub fn from_json(json: &str) -> Result<Query, ParseError> {
        let query: Query = serde_json::from_str(json)?;
        Self::validate(&query)?;
        Ok(query)
    }

    /// 自動判定してパース (YAMLを優先)
    pub fn parse(input: &str) -> Result<Query, ParseError> {
        // JSONっぽければJSONとして試す
        let trimmed = input.trim();
        if trimmed.starts_with('{') {
            return Self::from_json(input);
        }
        Self::from_yaml(input)
    }

    /// CLI簡易形式からパース
    ///
    /// 例:
    /// - `fn process` → kind: Function, name: "process"
    /// - `struct *Config` → kind: Struct, name: { glob: "*Config" }
    pub fn from_cli(kind_str: &str, pattern: &str) -> Result<Query, ParseError> {
        let kind = Self::parse_kind(kind_str)?;
        let name_matcher = Self::parse_name_pattern(pattern);

        Ok(Query {
            kind,
            r#match: Some(MatchAttrs {
                name: Some(name_matcher),
                ..Default::default()
            }),
            inner: vec![],
            queries: vec![],
            name: None,
            resolve: None,
            scope: None,
            view: None,
            limit: None,
            body: None,
            relations: None,
        })
    }

    /// kind文字列をパース
    fn parse_kind(s: &str) -> Result<QueryKind, ParseError> {
        match s.to_lowercase().as_str() {
            "any" | "*" => Ok(QueryKind::Any),
            "fn" | "function" => Ok(QueryKind::Function),
            "struct" => Ok(QueryKind::Struct),
            "enum" => Ok(QueryKind::Enum),
            "trait" => Ok(QueryKind::Trait),
            "impl" => Ok(QueryKind::Impl),
            "mod" | "module" => Ok(QueryKind::Mod),
            "const" => Ok(QueryKind::Const),
            "static" => Ok(QueryKind::Static),
            "type" | "typealias" => Ok(QueryKind::TypeAlias),
            _ => Err(ParseError::UnknownKind(s.to_string())),
        }
    }

    /// 名前パターンをパース
    ///
    /// - `*Config` → glob
    /// - `process*` → glob
    /// - `*process*` → contains
    /// - `process` → exact
    fn parse_name_pattern(pattern: &str) -> NameMatcher {
        if pattern.contains('*') || pattern.contains('?') {
            // glob pattern
            NameMatcher::Detailed(crate::schema::NameMatcherDetailed {
                glob: Some(pattern.to_string()),
                contains: None,
                starts_with: None,
                ends_with: None,
                regex: None,
                ignore_case: None,
                ignore_word_separate: None,
            })
        } else {
            NameMatcher::Exact(pattern.to_string())
        }
    }

    /// クエリを検証
    fn validate(query: &Query) -> Result<(), ParseError> {
        match query.kind {
            QueryKind::Or | QueryKind::And => {
                if query.queries.is_empty() {
                    return Err(ParseError::Invalid(
                        "Or/And query requires 'queries' field".to_string(),
                    ));
                }
                for q in &query.queries {
                    Self::validate(q)?;
                }
            }
            QueryKind::Pattern => {
                if query.name.is_none() {
                    return Err(ParseError::Invalid(
                        "Pattern query requires 'name' field".to_string(),
                    ));
                }
            }
            _ => {
                // inner検証
                for q in &query.inner {
                    Self::validate(q)?;
                }
            }
        }
        Ok(())
    }
}

/// ViewMode文字列パース
pub fn parse_view_mode(s: &str) -> Option<ViewMode> {
    match s.to_lowercase().as_str() {
        "snippet" => Some(ViewMode::Snippet),
        "precise" => Some(ViewMode::Precise),
        "count" => Some(ViewMode::Count),
        "def" => Some(ViewMode::Def),
        _ => None,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_yaml() {
        let yaml = r#"
kind: Function
match:
  name: "process"
"#;
        let query = QueryParser::from_yaml(yaml).unwrap();
        assert_eq!(query.kind, QueryKind::Function);
    }

    #[test]
    fn test_parse_json() {
        let json = r#"{"kind": "Struct", "match": {"name": "Config"}}"#;
        let query = QueryParser::from_json(json).unwrap();
        assert_eq!(query.kind, QueryKind::Struct);
    }

    #[test]
    fn test_auto_detect_json() {
        let json = r#"{"kind": "Enum"}"#;
        let query = QueryParser::parse(json).unwrap();
        assert_eq!(query.kind, QueryKind::Enum);
    }

    #[test]
    fn test_auto_detect_yaml() {
        let yaml = "kind: Trait\n";
        let query = QueryParser::parse(yaml).unwrap();
        assert_eq!(query.kind, QueryKind::Trait);
    }

    #[test]
    fn test_from_cli_exact() {
        let query = QueryParser::from_cli("fn", "process").unwrap();
        assert_eq!(query.kind, QueryKind::Function);
        let m = query.r#match.unwrap();
        assert!(matches!(m.name, Some(NameMatcher::Exact(ref s)) if s == "process"));
    }

    #[test]
    fn test_from_cli_glob() {
        let query = QueryParser::from_cli("struct", "*Config").unwrap();
        assert_eq!(query.kind, QueryKind::Struct);
        let m = query.r#match.unwrap();
        assert!(matches!(m.name, Some(NameMatcher::Detailed(_))));
    }

    #[test]
    fn test_validate_or_query() {
        let yaml = r#"
kind: Or
queries: []
"#;
        let result = QueryParser::from_yaml(yaml);
        assert!(result.is_err());
    }

    #[test]
    fn test_validate_pattern_query() {
        let yaml = r#"
kind: Pattern
"#;
        let result = QueryParser::from_yaml(yaml);
        assert!(result.is_err());
    }

    #[test]
    fn test_parse_view_mode() {
        assert_eq!(parse_view_mode("snippet"), Some(ViewMode::Snippet));
        assert_eq!(parse_view_mode("PRECISE"), Some(ViewMode::Precise));
        assert_eq!(parse_view_mode("count"), Some(ViewMode::Count));
        assert_eq!(parse_view_mode("def"), Some(ViewMode::Def));
        assert_eq!(parse_view_mode("DEF"), Some(ViewMode::Def));
        assert_eq!(parse_view_mode("invalid"), None);
    }
}