Skip to main content

sqry_core/schema/
cycle.rs

1//! Canonical cycle kind enumeration.
2//!
3//! Defines types of cycles to detect in code graphs.
4
5use serde::{Deserialize, Serialize};
6use std::fmt;
7
8/// Types of cycles to detect in code graphs.
9///
10/// Used by `find_cycles` and `is_node_in_cycle` tools to specify
11/// which type of cyclic dependencies to find.
12///
13/// # Serialization
14///
15/// All variants serialize to lowercase: `"calls"`, `"imports"`, etc.
16///
17/// # Examples
18///
19/// ```
20/// use sqry_core::schema::CycleKind;
21///
22/// let kind = CycleKind::Imports;
23/// assert_eq!(kind.as_str(), "imports");
24///
25/// let parsed = CycleKind::parse("modules").unwrap();
26/// assert_eq!(parsed, CycleKind::Modules);
27/// ```
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
29#[serde(rename_all = "lowercase")]
30#[derive(Default)]
31pub enum CycleKind {
32    /// Call cycles (A calls B, B calls A).
33    ///
34    /// Detects recursive call patterns that may indicate
35    /// infinite recursion or circular dependencies.
36    #[default]
37    Calls,
38
39    /// Import cycles (A imports B, B imports A).
40    ///
41    /// Detects circular import dependencies that can cause
42    /// initialization order problems in many languages.
43    Imports,
44
45    /// Module-level cycles (module A depends on module B, B depends on A).
46    ///
47    /// Higher-level view of circular dependencies at the
48    /// module/package level rather than individual symbols.
49    Modules,
50}
51
52impl CycleKind {
53    /// Returns all variants in definition order.
54    #[must_use]
55    pub const fn all() -> &'static [Self] {
56        &[Self::Calls, Self::Imports, Self::Modules]
57    }
58
59    /// Returns the canonical string representation.
60    #[must_use]
61    pub const fn as_str(self) -> &'static str {
62        match self {
63            Self::Calls => "calls",
64            Self::Imports => "imports",
65            Self::Modules => "modules",
66        }
67    }
68
69    /// Parses a string into a `CycleKind`.
70    ///
71    /// Returns `None` if the string doesn't match any known kind.
72    /// Case-insensitive.
73    #[must_use]
74    pub fn parse(s: &str) -> Option<Self> {
75        match s.to_lowercase().as_str() {
76            "calls" | "call" | "function" => Some(Self::Calls),
77            "imports" | "import" => Some(Self::Imports),
78            "modules" | "module" | "package" => Some(Self::Modules),
79            _ => None,
80        }
81    }
82
83    /// Returns a human-readable description of this cycle kind.
84    #[must_use]
85    pub const fn description(self) -> &'static str {
86        match self {
87            Self::Calls => "function/method call cycles",
88            Self::Imports => "import/dependency cycles",
89            Self::Modules => "module-level dependency cycles",
90        }
91    }
92}
93
94impl fmt::Display for CycleKind {
95    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96        f.write_str(self.as_str())
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn test_as_str() {
106        assert_eq!(CycleKind::Calls.as_str(), "calls");
107        assert_eq!(CycleKind::Imports.as_str(), "imports");
108        assert_eq!(CycleKind::Modules.as_str(), "modules");
109    }
110
111    #[test]
112    fn test_parse() {
113        assert_eq!(CycleKind::parse("calls"), Some(CycleKind::Calls));
114        assert_eq!(CycleKind::parse("IMPORTS"), Some(CycleKind::Imports));
115        assert_eq!(CycleKind::parse("call"), Some(CycleKind::Calls));
116        assert_eq!(CycleKind::parse("package"), Some(CycleKind::Modules));
117        assert_eq!(CycleKind::parse("unknown"), None);
118    }
119
120    #[test]
121    fn test_display() {
122        assert_eq!(format!("{}", CycleKind::Calls), "calls");
123        assert_eq!(format!("{}", CycleKind::Modules), "modules");
124    }
125
126    #[test]
127    fn test_serde_roundtrip() {
128        for kind in CycleKind::all() {
129            let json = serde_json::to_string(kind).unwrap();
130            let deserialized: CycleKind = serde_json::from_str(&json).unwrap();
131            assert_eq!(*kind, deserialized);
132        }
133    }
134
135    #[test]
136    fn test_default() {
137        assert_eq!(CycleKind::default(), CycleKind::Calls);
138    }
139
140    #[test]
141    fn test_description() {
142        assert!(CycleKind::Calls.description().contains("call"));
143        assert!(CycleKind::Imports.description().contains("import"));
144        assert!(CycleKind::Modules.description().contains("module"));
145    }
146}