prax_schema/loader/
discovery.rs1use std::path::{Path, PathBuf};
4
5use walkdir::WalkDir;
6
7use crate::error::{SchemaError, SchemaResult};
8
9#[derive(Debug, Clone)]
11pub struct Discovered {
12 pub absolute: PathBuf,
14 pub relative: PathBuf,
16}
17
18pub fn discover(root: impl AsRef<Path>) -> SchemaResult<Vec<Discovered>> {
26 let root = root.as_ref();
27 let canonical_root = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
28
29 let mut out = Vec::new();
30 for entry in WalkDir::new(&canonical_root)
31 .follow_links(false)
32 .into_iter()
33 .filter_entry(|e| !is_skipped(e))
34 {
35 let entry = entry.map_err(|e| SchemaError::IoError {
36 path: e
37 .path()
38 .map(|p| p.display().to_string())
39 .unwrap_or_default(),
40 source: e
41 .into_io_error()
42 .unwrap_or_else(|| std::io::Error::other("walkdir error")),
43 })?;
44
45 if !entry.file_type().is_file() {
46 continue;
47 }
48 if entry.path().extension().and_then(|s| s.to_str()) != Some("prax") {
49 continue;
50 }
51
52 let relative = entry
53 .path()
54 .strip_prefix(&canonical_root)
55 .unwrap_or(entry.path())
56 .to_path_buf();
57
58 out.push(Discovered {
59 absolute: entry.path().to_path_buf(),
60 relative,
61 });
62 }
63
64 out.sort_by(|a, b| a.relative.cmp(&b.relative));
65 Ok(out)
66}
67
68fn is_skipped(entry: &walkdir::DirEntry) -> bool {
69 if entry.depth() == 0 {
71 return false;
72 }
73 if let Some(name) = entry.file_name().to_str() {
74 if name.starts_with('.') {
75 return true;
76 }
77 if entry.file_type().is_dir() && name == "target" {
78 return true;
79 }
80 }
81 if entry.file_type().is_symlink() {
82 return true;
83 }
84 false
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use std::fs;
91 use tempfile::tempdir;
92
93 fn write(dir: &Path, name: &str, content: &str) {
94 let p = dir.join(name);
95 if let Some(parent) = p.parent() {
96 fs::create_dir_all(parent).unwrap();
97 }
98 fs::write(p, content).unwrap();
99 }
100
101 #[test]
102 fn flat_directory_returns_sorted_prax_files() {
103 let dir = tempdir().unwrap();
104 write(dir.path(), "b.prax", "// b");
105 write(dir.path(), "a.prax", "// a");
106 write(dir.path(), "c.prax", "// c");
107
108 let found = discover(dir.path()).unwrap();
109 let names: Vec<_> = found.iter().map(|d| d.relative.to_str().unwrap()).collect();
110 assert_eq!(names, vec!["a.prax", "b.prax", "c.prax"]);
111 }
112
113 #[test]
114 fn recursive_descent_finds_nested_files() {
115 let dir = tempdir().unwrap();
116 write(dir.path(), "schema.prax", "// root");
117 write(dir.path(), "models/user.prax", "model U {}");
118 write(dir.path(), "models/post.prax", "model P {}");
119 write(dir.path(), "enums/role.prax", "enum R {}");
120
121 let found = discover(dir.path()).unwrap();
122 assert_eq!(found.len(), 4);
123 }
124
125 #[test]
126 fn hidden_dirs_are_skipped() {
127 let dir = tempdir().unwrap();
128 write(dir.path(), "ok.prax", "// ok");
129 write(dir.path(), ".git/HEAD", "// not prax");
130 write(dir.path(), ".cache/bad.prax", "// skipped");
131
132 let found = discover(dir.path()).unwrap();
133 assert_eq!(found.len(), 1);
134 assert_eq!(found[0].relative.to_str().unwrap(), "ok.prax");
135 }
136
137 #[test]
138 fn target_directory_is_skipped() {
139 let dir = tempdir().unwrap();
140 write(dir.path(), "ok.prax", "// ok");
141 write(dir.path(), "target/build.prax", "// skipped");
142
143 let found = discover(dir.path()).unwrap();
144 assert_eq!(found.len(), 1);
145 }
146
147 #[test]
148 fn non_prax_files_ignored() {
149 let dir = tempdir().unwrap();
150 write(dir.path(), "ok.prax", "// ok");
151 write(dir.path(), "README.md", "# readme");
152 write(dir.path(), "schema.prisma", "// wrong ext");
153
154 let found = discover(dir.path()).unwrap();
155 assert_eq!(found.len(), 1);
156 }
157}