ryo-pattern 0.1.0

RyoPattern - AST pattern matching and lint rules for Ryo
Documentation
//! Relation queries with logical grouping
//!
//! Provides `any`/`all`/`none` logical operators for relation-based filtering.

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

/// Relation conditions with logical grouping
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct Relations {
    /// At least one relation matches (OR)
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub any: Option<Vec<Relation>>,

    /// Every relation must match (AND)
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub all: Option<Vec<Relation>>,

    /// No relation matches (NOT)
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub none: Option<Vec<Relation>>,
}

impl Relations {
    /// Construct an empty `Relations` set.
    pub fn new() -> Self {
        Self::default()
    }

    /// Add a relation to `any` group
    pub fn any(mut self, relation: Relation) -> Self {
        self.any.get_or_insert_with(Vec::new).push(relation);
        self
    }

    /// Add a relation to `all` group
    pub fn all(mut self, relation: Relation) -> Self {
        self.all.get_or_insert_with(Vec::new).push(relation);
        self
    }

    /// Add a relation to `none` group
    pub fn none(mut self, relation: Relation) -> Self {
        self.none.get_or_insert_with(Vec::new).push(relation);
        self
    }

    /// Check if relations is empty
    pub fn is_empty(&self) -> bool {
        self.any.is_none() && self.all.is_none() && self.none.is_none()
    }
}

/// Single relation condition
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct Relation {
    /// Relation type
    pub kind: RelationKind,

    /// Target query (simplified for now - will be expanded)
    pub target: RelationTarget,

    /// Include transitive relations
    #[serde(default)]
    pub transitive: bool,

    /// Maximum traversal depth for transitive queries (None = unlimited)
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub max_depth: Option<u32>,

    /// Traversal scope for transitive queries
    #[serde(default)]
    pub scope: TraversalScope,

    /// Bind result to variable
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub r#as: Option<String>,
}

impl Relation {
    /// Create a new relation
    pub fn new(kind: RelationKind, target: RelationTarget) -> Self {
        Self {
            kind,
            target,
            transitive: false,
            max_depth: None,
            scope: TraversalScope::default(),
            r#as: None,
        }
    }

    /// Set transitive mode
    pub fn transitive(mut self) -> Self {
        self.transitive = true;
        self
    }

    /// Set max depth
    pub fn with_max_depth(mut self, depth: u32) -> Self {
        self.max_depth = Some(depth);
        self
    }

    /// Set scope
    pub fn with_scope(mut self, scope: TraversalScope) -> Self {
        self.scope = scope;
        self
    }

    /// Bind to variable
    pub fn bind(mut self, var: impl Into<String>) -> Self {
        self.r#as = Some(var.into());
        self
    }
}

/// Simplified target for relation queries
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct RelationTarget {
    /// Symbol kind to match
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub kind: Option<TargetKind>,

    /// Name match condition
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub r#match: Option<TargetMatch>,
}

impl RelationTarget {
    /// Construct an empty `RelationTarget` (no kind / no match).
    pub fn new() -> Self {
        Self {
            kind: None,
            r#match: None,
        }
    }

    /// Set the target symbol kind.
    pub fn kind(mut self, kind: TargetKind) -> Self {
        self.kind = Some(kind);
        self
    }

    /// Set the target symbol name (exact match).
    pub fn name(mut self, name: impl Into<String>) -> Self {
        self.r#match = Some(TargetMatch {
            name: Some(name.into()),
            ..Default::default()
        });
        self
    }
}

impl Default for RelationTarget {
    fn default() -> Self {
        Self::new()
    }
}

/// Target symbol kind
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub enum TargetKind {
    /// `fn` item.
    Function,
    /// `struct` item.
    Struct,
    /// `enum` item.
    Enum,
    /// `trait` item.
    Trait,
    /// `impl` block.
    Impl,
    /// `mod` item.
    Mod,
    /// `const` item.
    Const,
    /// `static` item.
    Static,
    /// `type` alias.
    TypeAlias,
}

/// Target match conditions
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct TargetMatch {
    /// Exact name or pattern
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,

    /// Glob pattern for name
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub pattern: Option<String>,

    /// Regex pattern for name
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub regex: Option<String>,

    /// Required attributes
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub attributes: Option<Vec<String>>,
}

/// Traversal scope for transitive queries
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub enum TraversalScope {
    /// Only symbols within the workspace
    #[default]
    Workspace,
    /// Include external dependencies
    All,
}

/// Relation kind enumeration.
///
/// Three axes of symbol usage:
/// - **Call**: `Calls` / `CalledBy` — function call relationships
/// - **Type**: `TypeReferences` / `TypeReferencedBy` — type usage relationships
/// - **Trait**: `Implements` / `ImplementedBy` — trait implementation relationships
///
/// Plus structural containment: `Contains` / `ContainedBy`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
pub enum RelationKind {
    /// Source calls target (function call)
    Calls,
    /// Source is called by target
    CalledBy,
    /// Source references target as a type (field type, param type, return type, etc.)
    TypeReferences,
    /// Source's type is referenced by target
    TypeReferencedBy,
    /// Source implements target trait
    Implements,
    /// Source trait is implemented by target
    ImplementedBy,
    /// Source contains target (parent-child)
    Contains,
    /// Source is contained by target
    ContainedBy,
}

impl RelationKind {
    /// Get the reverse relation kind
    pub fn reverse(&self) -> Self {
        match self {
            Self::Calls => Self::CalledBy,
            Self::CalledBy => Self::Calls,
            Self::TypeReferences => Self::TypeReferencedBy,
            Self::TypeReferencedBy => Self::TypeReferences,
            Self::Implements => Self::ImplementedBy,
            Self::ImplementedBy => Self::Implements,
            Self::Contains => Self::ContainedBy,
            Self::ContainedBy => Self::Contains,
        }
    }
}

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

    #[test]
    fn test_relations_builder() {
        let rels = Relations::new()
            .any(Relation::new(
                RelationKind::Calls,
                RelationTarget::new()
                    .kind(TargetKind::Function)
                    .name("unwrap"),
            ))
            .none(Relation::new(
                RelationKind::TypeReferences,
                RelationTarget::new()
                    .kind(TargetKind::Struct)
                    .name("Database"),
            ));

        assert!(rels.any.is_some());
        assert!(rels.none.is_some());
        assert!(rels.all.is_none());
    }

    #[test]
    fn test_relation_transitive() {
        let rel = Relation::new(RelationKind::CalledBy, RelationTarget::new())
            .transitive()
            .with_max_depth(10)
            .with_scope(TraversalScope::Workspace);

        assert!(rel.transitive);
        assert_eq!(rel.max_depth, Some(10));
        assert_eq!(rel.scope, TraversalScope::Workspace);
    }

    #[test]
    fn test_relation_kind_reverse() {
        assert_eq!(RelationKind::Calls.reverse(), RelationKind::CalledBy);
        assert_eq!(
            RelationKind::TypeReferences.reverse(),
            RelationKind::TypeReferencedBy
        );
        assert_eq!(
            RelationKind::Implements.reverse(),
            RelationKind::ImplementedBy
        );
    }
}