tessera_codegraph/
config.rs1use std::fs;
2use std::path::Path;
3
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Default, Serialize, Deserialize)]
7pub struct TesseraConfig {
8 pub include_paths: Vec<String>,
10 pub exclude_paths: Vec<String>,
12 pub ignore_names: Vec<String>,
14}
15
16impl TesseraConfig {
17 pub fn load(root: &Path) -> Self {
18 let path = root.join(".tessera/config.toml");
19 let Ok(content) = fs::read_to_string(path) else {
20 return Self::default();
21 };
22 Self::parse_lossy(&content)
23 }
24
25 pub fn parse_lossy(content: &str) -> Self {
26 let mut config = Self::default();
27 let mut section = String::new();
28
29 for raw_line in content.lines() {
30 let line = raw_line.split('#').next().unwrap_or_default().trim();
31 if line.is_empty() {
32 continue;
33 }
34 if line.starts_with('[') && line.ends_with(']') {
35 section = line.trim_matches(['[', ']']).trim().to_string();
36 continue;
37 }
38 let Some((key, value)) = line.split_once('=') else {
39 continue;
40 };
41 let key = key.trim();
42 let values = parse_string_array(value.trim());
43 match (section.as_str(), key) {
44 ("include", "paths") => config.include_paths = normalize_prefixes(values),
45 ("exclude", "paths") => config.exclude_paths = normalize_prefixes(values),
46 ("ignore", "extra") => config.ignore_names = values,
47 _ => {}
48 }
49 }
50
51 config
52 }
53
54 pub fn includes_path(&self, rel_path: &str) -> bool {
55 self.include_paths.is_empty()
56 || self
57 .include_paths
58 .iter()
59 .any(|prefix| rel_path.starts_with(prefix))
60 }
61
62 pub fn excludes_path(&self, rel_path: &str) -> bool {
63 self.exclude_paths
64 .iter()
65 .any(|prefix| rel_path.starts_with(prefix))
66 }
67
68 pub fn ignores_name(&self, name: &str) -> bool {
69 self.ignore_names.iter().any(|ignored| ignored == name)
70 }
71}
72
73fn parse_string_array(value: &str) -> Vec<String> {
74 let value = value.trim();
75 let Some(inner) = value.strip_prefix('[').and_then(|v| v.strip_suffix(']')) else {
76 return Vec::new();
77 };
78 inner
79 .split(',')
80 .filter_map(|part| {
81 let trimmed = part.trim().trim_matches('"').trim_matches('\'').trim();
82 if trimmed.is_empty() {
83 None
84 } else {
85 Some(trimmed.replace('\\', "/"))
86 }
87 })
88 .collect()
89}
90
91fn normalize_prefixes(values: Vec<String>) -> Vec<String> {
92 values
93 .into_iter()
94 .map(|value| value.trim_start_matches("./").replace('\\', "/"))
95 .collect()
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101
102 #[test]
103 fn parses_include_exclude_and_extra_ignores() {
104 let config = TesseraConfig::parse_lossy(
105 r#"
106[include]
107paths = ["src/", "lib"]
108
109[exclude]
110paths = ["src/generated/"]
111
112[ignore]
113extra = ["fixtures", "tmp"]
114"#,
115 );
116
117 assert!(config.includes_path("src/app.ts"));
118 assert!(!config.includes_path("tests/app.ts"));
119 assert!(config.excludes_path("src/generated/app.ts"));
120 assert!(config.ignores_name("fixtures"));
121 assert!(config.ignores_name("tmp"));
122 }
123}