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    /// Find error-chain wrap relationships (T3.6 / Cluster G).
59    ///
60    /// Traverses outbound `EdgeKind::Wraps` edges of any `WrapKind`
61    /// (`ErrorfVerb`, `UnwrapMethod`, `UnwrapMultiMethod`,
62    /// `ErrorsIs`, `ErrorsAs`, `ErrorsAsType`, `ErrorsJoin`). For
63    /// kind-filtered queries use the planner's `wraps:<kind>`
64    /// predicate.
65    Wraps,
66}
67
68impl RelationKind {
69    /// Returns all variants in definition order.
70    #[must_use]
71    pub const fn all() -> &'static [Self] {
72        &[
73            Self::Callers,
74            Self::Callees,
75            Self::Imports,
76            Self::Exports,
77            Self::Returns,
78            Self::Wraps,
79        ]
80    }
81
82    /// Returns the canonical string representation.
83    #[must_use]
84    pub const fn as_str(self) -> &'static str {
85        match self {
86            Self::Callers => "callers",
87            Self::Callees => "callees",
88            Self::Imports => "imports",
89            Self::Exports => "exports",
90            Self::Returns => "returns",
91            Self::Wraps => "wraps",
92        }
93    }
94
95    /// Parses a string into a `RelationKind`.
96    ///
97    /// Returns `None` if the string doesn't match any known kind.
98    /// Case-insensitive.
99    #[must_use]
100    pub fn parse(s: &str) -> Option<Self> {
101        match s.to_lowercase().as_str() {
102            "callers" => Some(Self::Callers),
103            "callees" => Some(Self::Callees),
104            "imports" => Some(Self::Imports),
105            "exports" => Some(Self::Exports),
106            "returns" => Some(Self::Returns),
107            "wraps" => Some(Self::Wraps),
108            _ => None,
109        }
110    }
111
112    /// Returns `true` if this relation traverses call edges.
113    #[must_use]
114    pub const fn is_call_relation(self) -> bool {
115        matches!(self, Self::Callers | Self::Callees)
116    }
117
118    /// Returns `true` if this relation traverses import/export edges.
119    #[must_use]
120    pub const fn is_boundary_relation(self) -> bool {
121        matches!(self, Self::Imports | Self::Exports)
122    }
123}
124
125impl fmt::Display for RelationKind {
126    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127        f.write_str(self.as_str())
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn test_as_str() {
137        assert_eq!(RelationKind::Callers.as_str(), "callers");
138        assert_eq!(RelationKind::Callees.as_str(), "callees");
139        assert_eq!(RelationKind::Imports.as_str(), "imports");
140        assert_eq!(RelationKind::Exports.as_str(), "exports");
141        assert_eq!(RelationKind::Returns.as_str(), "returns");
142    }
143
144    #[test]
145    fn test_parse() {
146        assert_eq!(RelationKind::parse("callers"), Some(RelationKind::Callers));
147        assert_eq!(RelationKind::parse("CALLEES"), Some(RelationKind::Callees));
148        assert_eq!(RelationKind::parse("Imports"), Some(RelationKind::Imports));
149        assert_eq!(RelationKind::parse("unknown"), None);
150    }
151
152    #[test]
153    fn test_display() {
154        assert_eq!(format!("{}", RelationKind::Callers), "callers");
155        assert_eq!(format!("{}", RelationKind::Returns), "returns");
156    }
157
158    #[test]
159    fn test_serde_roundtrip() {
160        for kind in RelationKind::all() {
161            let json = serde_json::to_string(kind).unwrap();
162            let deserialized: RelationKind = serde_json::from_str(&json).unwrap();
163            assert_eq!(*kind, deserialized);
164        }
165    }
166
167    #[test]
168    fn test_classification() {
169        assert!(RelationKind::Callers.is_call_relation());
170        assert!(RelationKind::Callees.is_call_relation());
171        assert!(!RelationKind::Imports.is_call_relation());
172
173        assert!(RelationKind::Imports.is_boundary_relation());
174        assert!(RelationKind::Exports.is_boundary_relation());
175        assert!(!RelationKind::Callers.is_boundary_relation());
176    }
177}