Skip to main content

sqry_core/schema/
relation.rs

1//! Canonical relation kind enumeration.
2//!
3//! Defines the types of symbol relationships that can be queried.
4
5use serde::{Deserialize, Serialize};
6use std::fmt;
7
8/// Types of symbol relationships for relation queries.
9///
10/// Used by the `relation_query` tool/command to specify which
11/// relationships to traverse from a given symbol.
12///
13/// # Serialization
14///
15/// All variants serialize to lowercase: `"callers"`, `"callees"`, etc.
16///
17/// # Examples
18///
19/// ```
20/// use sqry_core::schema::RelationKind;
21///
22/// let kind = RelationKind::Callers;
23/// assert_eq!(kind.as_str(), "callers");
24///
25/// let parsed = RelationKind::parse("callees").unwrap();
26/// assert_eq!(parsed, RelationKind::Callees);
27/// ```
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
29#[serde(rename_all = "lowercase")]
30#[derive(Default)]
31pub enum RelationKind {
32    /// Find symbols that call the target symbol.
33    ///
34    /// Traverses incoming `Calls` edges in the graph.
35    #[default]
36    Callers,
37
38    /// Find symbols that the target symbol calls.
39    ///
40    /// Traverses outgoing `Calls` edges in the graph.
41    Callees,
42
43    /// Find symbols imported by the target symbol/file.
44    ///
45    /// Traverses `Imports` edges in the graph.
46    Imports,
47
48    /// Find symbols exported by the target symbol/file.
49    ///
50    /// Traverses `Exports` edges in the graph.
51    Exports,
52
53    /// Find return type relationships.
54    ///
55    /// Traverses `TypeOf` edges where the source is a function/method.
56    Returns,
57}
58
59impl RelationKind {
60    /// Returns all variants in definition order.
61    #[must_use]
62    pub const fn all() -> &'static [Self] {
63        &[
64            Self::Callers,
65            Self::Callees,
66            Self::Imports,
67            Self::Exports,
68            Self::Returns,
69        ]
70    }
71
72    /// Returns the canonical string representation.
73    #[must_use]
74    pub const fn as_str(self) -> &'static str {
75        match self {
76            Self::Callers => "callers",
77            Self::Callees => "callees",
78            Self::Imports => "imports",
79            Self::Exports => "exports",
80            Self::Returns => "returns",
81        }
82    }
83
84    /// Parses a string into a `RelationKind`.
85    ///
86    /// Returns `None` if the string doesn't match any known kind.
87    /// Case-insensitive.
88    #[must_use]
89    pub fn parse(s: &str) -> Option<Self> {
90        match s.to_lowercase().as_str() {
91            "callers" => Some(Self::Callers),
92            "callees" => Some(Self::Callees),
93            "imports" => Some(Self::Imports),
94            "exports" => Some(Self::Exports),
95            "returns" => Some(Self::Returns),
96            _ => None,
97        }
98    }
99
100    /// Returns `true` if this relation traverses call edges.
101    #[must_use]
102    pub const fn is_call_relation(self) -> bool {
103        matches!(self, Self::Callers | Self::Callees)
104    }
105
106    /// Returns `true` if this relation traverses import/export edges.
107    #[must_use]
108    pub const fn is_boundary_relation(self) -> bool {
109        matches!(self, Self::Imports | Self::Exports)
110    }
111}
112
113impl fmt::Display for RelationKind {
114    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115        f.write_str(self.as_str())
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn test_as_str() {
125        assert_eq!(RelationKind::Callers.as_str(), "callers");
126        assert_eq!(RelationKind::Callees.as_str(), "callees");
127        assert_eq!(RelationKind::Imports.as_str(), "imports");
128        assert_eq!(RelationKind::Exports.as_str(), "exports");
129        assert_eq!(RelationKind::Returns.as_str(), "returns");
130    }
131
132    #[test]
133    fn test_parse() {
134        assert_eq!(RelationKind::parse("callers"), Some(RelationKind::Callers));
135        assert_eq!(RelationKind::parse("CALLEES"), Some(RelationKind::Callees));
136        assert_eq!(RelationKind::parse("Imports"), Some(RelationKind::Imports));
137        assert_eq!(RelationKind::parse("unknown"), None);
138    }
139
140    #[test]
141    fn test_display() {
142        assert_eq!(format!("{}", RelationKind::Callers), "callers");
143        assert_eq!(format!("{}", RelationKind::Returns), "returns");
144    }
145
146    #[test]
147    fn test_serde_roundtrip() {
148        for kind in RelationKind::all() {
149            let json = serde_json::to_string(kind).unwrap();
150            let deserialized: RelationKind = serde_json::from_str(&json).unwrap();
151            assert_eq!(*kind, deserialized);
152        }
153    }
154
155    #[test]
156    fn test_classification() {
157        assert!(RelationKind::Callers.is_call_relation());
158        assert!(RelationKind::Callees.is_call_relation());
159        assert!(!RelationKind::Imports.is_call_relation());
160
161        assert!(RelationKind::Imports.is_boundary_relation());
162        assert!(RelationKind::Exports.is_boundary_relation());
163        assert!(!RelationKind::Callers.is_boundary_relation());
164    }
165}