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    /// Find channel send / receive / close operation sites on a channel
68    /// (Go T2.4).
69    ///
70    /// Traverses `EdgeKind::ChannelPeer` edges anchored on a `Channel`
71    /// node (or expanded from a containing function's body). The
72    /// container-level `rename_all = "lowercase"` would serialize this as
73    /// `"channelpeers"`, so an explicit per-variant rename pins the
74    /// `"channel_peers"` wire string.
75    #[serde(rename = "channel_peers")]
76    ChannelPeers,
77
78    /// Find generic-instantiation call sites of a generic function /
79    /// method (Go T2.5).
80    ///
81    /// Traverses `EdgeKind::Instantiates` edges. The explicit rename is
82    /// redundant (`Instantiations` already lowercases to
83    /// `"instantiations"`) but kept for symmetry with `ChannelPeers`.
84    #[serde(rename = "instantiations")]
85    Instantiations,
86}
87
88impl RelationKind {
89    /// Returns all variants in definition order.
90    #[must_use]
91    pub const fn all() -> &'static [Self] {
92        &[
93            Self::Callers,
94            Self::Callees,
95            Self::Imports,
96            Self::Exports,
97            Self::Returns,
98            Self::Wraps,
99            Self::ChannelPeers,
100            Self::Instantiations,
101        ]
102    }
103
104    /// Returns the canonical string representation.
105    #[must_use]
106    pub const fn as_str(self) -> &'static str {
107        match self {
108            Self::Callers => "callers",
109            Self::Callees => "callees",
110            Self::Imports => "imports",
111            Self::Exports => "exports",
112            Self::Returns => "returns",
113            Self::Wraps => "wraps",
114            Self::ChannelPeers => "channel_peers",
115            Self::Instantiations => "instantiations",
116        }
117    }
118
119    /// Parses a string into a `RelationKind`.
120    ///
121    /// Returns `None` if the string doesn't match any known kind.
122    /// Case-insensitive.
123    #[must_use]
124    pub fn parse(s: &str) -> Option<Self> {
125        match s.to_lowercase().as_str() {
126            "callers" => Some(Self::Callers),
127            "callees" => Some(Self::Callees),
128            "imports" => Some(Self::Imports),
129            "exports" => Some(Self::Exports),
130            "returns" => Some(Self::Returns),
131            "wraps" => Some(Self::Wraps),
132            "channel_peers" => Some(Self::ChannelPeers),
133            "instantiations" => Some(Self::Instantiations),
134            _ => None,
135        }
136    }
137
138    /// Returns `true` if this relation traverses call edges.
139    #[must_use]
140    pub const fn is_call_relation(self) -> bool {
141        matches!(self, Self::Callers | Self::Callees)
142    }
143
144    /// Returns `true` if this relation traverses import/export edges.
145    #[must_use]
146    pub const fn is_boundary_relation(self) -> bool {
147        matches!(self, Self::Imports | Self::Exports)
148    }
149}
150
151impl fmt::Display for RelationKind {
152    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153        f.write_str(self.as_str())
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn test_as_str() {
163        assert_eq!(RelationKind::Callers.as_str(), "callers");
164        assert_eq!(RelationKind::Callees.as_str(), "callees");
165        assert_eq!(RelationKind::Imports.as_str(), "imports");
166        assert_eq!(RelationKind::Exports.as_str(), "exports");
167        assert_eq!(RelationKind::Returns.as_str(), "returns");
168    }
169
170    #[test]
171    fn test_parse() {
172        assert_eq!(RelationKind::parse("callers"), Some(RelationKind::Callers));
173        assert_eq!(RelationKind::parse("CALLEES"), Some(RelationKind::Callees));
174        assert_eq!(RelationKind::parse("Imports"), Some(RelationKind::Imports));
175        assert_eq!(RelationKind::parse("unknown"), None);
176    }
177
178    #[test]
179    fn test_new_relation_kinds_wire_strings() {
180        // The wire strings are contract-locked: ChannelPeers must NOT
181        // serialize as "channelpeers" (the lowercase rename_all default).
182        assert_eq!(RelationKind::ChannelPeers.as_str(), "channel_peers");
183        assert_eq!(RelationKind::Instantiations.as_str(), "instantiations");
184        assert_eq!(
185            RelationKind::parse("channel_peers"),
186            Some(RelationKind::ChannelPeers)
187        );
188        assert_eq!(
189            RelationKind::parse("instantiations"),
190            Some(RelationKind::Instantiations)
191        );
192        // serde wire shape (used by the MCP / CLI JSON surface).
193        assert_eq!(
194            serde_json::to_string(&RelationKind::ChannelPeers).unwrap(),
195            "\"channel_peers\""
196        );
197        assert_eq!(
198            serde_json::to_string(&RelationKind::Instantiations).unwrap(),
199            "\"instantiations\""
200        );
201        // Neither new relation is a call or boundary relation.
202        assert!(!RelationKind::ChannelPeers.is_call_relation());
203        assert!(!RelationKind::Instantiations.is_boundary_relation());
204    }
205
206    #[test]
207    fn test_display() {
208        assert_eq!(format!("{}", RelationKind::Callers), "callers");
209        assert_eq!(format!("{}", RelationKind::Returns), "returns");
210    }
211
212    #[test]
213    fn test_serde_roundtrip() {
214        for kind in RelationKind::all() {
215            let json = serde_json::to_string(kind).unwrap();
216            let deserialized: RelationKind = serde_json::from_str(&json).unwrap();
217            assert_eq!(*kind, deserialized);
218        }
219    }
220
221    #[test]
222    fn test_classification() {
223        assert!(RelationKind::Callers.is_call_relation());
224        assert!(RelationKind::Callees.is_call_relation());
225        assert!(!RelationKind::Imports.is_call_relation());
226
227        assert!(RelationKind::Imports.is_boundary_relation());
228        assert!(RelationKind::Exports.is_boundary_relation());
229        assert!(!RelationKind::Callers.is_boundary_relation());
230    }
231}