rust_relations_explorer/utils/
mod.rs

1// Utilities module placeholder per design
2pub mod table {
3    // Helper to render a separator line
4    fn sep(widths: &[usize]) -> String {
5        let mut s = String::from("+");
6        for w in widths {
7            s.push_str(&"-".repeat(w + 2));
8            s.push('+');
9        }
10        s
11    }
12
13    // Helper to render a row line
14    fn line(cells: &[String], widths: &[usize]) -> String {
15        let mut s = String::from("|");
16        for (i, cell) in cells.iter().enumerate() {
17            let w = widths[i];
18            s.push(' ');
19            s.push_str(cell);
20            if cell.len() < w {
21                s.push_str(&" ".repeat(w - cell.len()));
22            }
23            s.push(' ');
24            s.push('|');
25        }
26        s
27    }
28
29    // Render a simple ASCII table given headers and rows
30    pub fn render(headers: &[&str], rows: &[Vec<String>]) -> String {
31        let cols = headers.len();
32        let mut widths: Vec<usize> = headers.iter().map(|h| h.len()).collect();
33        for row in rows {
34            for (c, w) in widths.iter_mut().enumerate().take(cols) {
35                *w = (*w).max(row.get(c).map_or(0, String::len));
36            }
37        }
38
39        let mut out = String::new();
40        out.push_str(&sep(&widths));
41        out.push('\n');
42        let header_cells: Vec<String> = headers.iter().map(|s| (*s).to_string()).collect();
43        out.push_str(&line(&header_cells, &widths));
44        out.push('\n');
45        out.push_str(&sep(&widths));
46        out.push('\n');
47        for row in rows {
48            let mut cells = Vec::with_capacity(cols);
49            for i in 0..cols {
50                cells.push(row.get(i).cloned().unwrap_or_default());
51            }
52            out.push_str(&line(&cells, &widths));
53            out.push('\n');
54        }
55        out.push_str(&sep(&widths));
56        out
57    }
58}
59
60pub mod config {
61    use serde::Deserialize;
62    use std::fs;
63    use std::path::{Path, PathBuf};
64
65    #[derive(Debug, Clone, Deserialize, Default)]
66    pub struct DotConfig {
67        pub clusters: Option<bool>,
68        pub legend: Option<bool>,
69        pub theme: Option<String>,   // "light" | "dark"
70        pub rankdir: Option<String>, // "LR" | "TB"
71        pub splines: Option<String>, // "curved" | "ortho" | "polyline"
72        pub rounded: Option<bool>,
73    }
74
75    #[derive(Debug, Clone, Deserialize, Default)]
76    pub struct SvgConfig {
77        pub interactive: Option<bool>,
78    }
79
80    #[derive(Debug, Clone, Deserialize, Default)]
81    pub struct QueryConfig {
82        pub default_format: Option<String>, // "text" | "json"
83    }
84
85    #[derive(Debug, Clone, Deserialize, Default)]
86    pub struct Config {
87        pub root: Option<String>,
88        pub dot: Option<DotConfig>,
89        pub svg: Option<SvgConfig>,
90        pub query: Option<QueryConfig>,
91    }
92
93    fn default_config_path(root: &Path) -> PathBuf {
94        // Prefer new name, keep backward compatibility with old filename
95        root.join("rust-relations-explorer.toml")
96    }
97
98    #[must_use]
99    pub fn load_config_at(path: &Path) -> Option<Config> {
100        let data = fs::read_to_string(path).ok()?;
101        toml::from_str::<Config>(&data).ok()
102    }
103
104    #[must_use]
105    pub fn load_config_near(root: &Path) -> Option<Config> {
106        let p_new = default_config_path(root);
107        if p_new.exists() {
108            return load_config_at(&p_new);
109        }
110        let p_old = root.join("knowledge-rs.toml");
111        if p_old.exists() {
112            load_config_at(&p_old)
113        } else {
114            None
115        }
116    }
117}
118
119pub mod cache {
120    use serde::{Deserialize, Serialize};
121    use std::collections::HashMap;
122    use std::path::{Path, PathBuf};
123
124    use crate::graph::FileNode;
125
126    #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
127    pub struct CacheEntryMeta {
128        pub mtime: u64,
129        pub len: u64,
130    }
131
132    #[derive(Debug, Clone, Serialize, Deserialize, Default)]
133    pub struct CacheEntry {
134        pub meta: CacheEntryMeta,
135        pub node: FileNode,
136    }
137
138    #[derive(Debug, Clone, Serialize, Deserialize, Default)]
139    pub struct Cache {
140        pub entries: HashMap<PathBuf, CacheEntry>,
141    }
142
143    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
144    pub enum CacheMode {
145        Use,
146        Ignore,
147        Rebuild,
148    }
149
150    fn cache_path(root: &Path) -> PathBuf {
151        root.join(".knowledge_cache.json")
152    }
153
154    #[must_use]
155    pub fn load_cache(root: &Path) -> Option<Cache> {
156        let path = cache_path(root);
157        let data = std::fs::read_to_string(path).ok()?;
158        serde_json::from_str::<Cache>(&data).ok()
159    }
160
161    pub fn save_cache(root: &Path, cache: &Cache) {
162        let path = cache_path(root);
163        if let Ok(data) = serde_json::to_string_pretty(cache) {
164            let _ = std::fs::write(path, data);
165        }
166    }
167
168    pub fn clear_cache(root: &Path) {
169        let path = cache_path(root);
170        let _ = std::fs::remove_file(path);
171    }
172}
173
174pub mod file_walker {
175    /// Discover Rust source files under `root`, with an option to bypass ignore rules.
176    #[must_use]
177    pub fn rust_files_with_options(root: &str, no_ignore: bool) -> Vec<String> {
178        let mut out = Vec::new();
179        let mut walker = ignore::WalkBuilder::new(root);
180        // Explicitly enable .gitignore/.ignore support and parent traversal (unless bypassed)
181        walker
182            .follow_links(false)
183            .git_ignore(!no_ignore)
184            .git_global(false)
185            .git_exclude(false)
186            .ignore(!no_ignore)
187            .parents(true);
188        // Build a Gitignore matcher from root-level ignore files for explicit checks (unless bypassed)
189        let root_path = std::path::Path::new(root);
190        let matcher = if no_ignore {
191            None
192        } else {
193            let mut gi_builder = ignore::gitignore::GitignoreBuilder::new(root_path);
194            let gi = root_path.join(".gitignore");
195            if gi.exists() {
196                let _ = gi_builder.add(gi);
197            }
198            let ign = root_path.join(".ignore");
199            if ign.exists() {
200                let _ = gi_builder.add(ign);
201            }
202            gi_builder.build().ok()
203        };
204        for entry in walker.build().flatten() {
205            if entry.file_type().is_some_and(|t| t.is_file()) {
206                // Explicit filter using matcher (in addition to WalkBuilder's own filtering)
207                if let Some(m) = &matcher {
208                    if m.matched(entry.path(), false).is_ignore() {
209                        continue;
210                    }
211                }
212                if entry.path().extension() == Some(std::ffi::OsStr::new("rs")) {
213                    if let Some(s) = entry.path().to_str() {
214                        out.push(s.to_string());
215                    }
216                }
217            }
218        }
219        out
220    }
221
222    /// Backward-compatible helper that reads env var and delegates to `rust_files_with_options`.
223    #[must_use]
224    pub fn rust_files(root: &str) -> Vec<String> {
225        let no_ignore = std::env::var("KNOWLEDGE_RS_NO_IGNORE")
226            .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
227            .unwrap_or(false);
228        rust_files_with_options(root, no_ignore)
229    }
230}