cuenv_workspaces/discovery/
mod.rs1use crate::error::{Error, Result};
30use glob::Pattern;
31use serde::de::DeserializeOwned;
32use std::collections::HashSet;
33use std::fs;
34use std::path::{Path, PathBuf};
35use walkdir::WalkDir;
36
37#[cfg(feature = "discovery-cargo")]
38pub mod cargo_toml;
39
40#[cfg(feature = "discovery-package-json")]
41pub mod package_json;
42
43#[cfg(feature = "discovery-pnpm")]
44pub mod pnpm_workspace;
45
46#[cfg(feature = "discovery-cargo")]
47pub use cargo_toml::CargoTomlDiscovery;
48
49#[cfg(feature = "discovery-package-json")]
50pub use package_json::PackageJsonDiscovery;
51
52#[cfg(feature = "discovery-pnpm")]
53pub use pnpm_workspace::PnpmWorkspaceDiscovery;
54
55pub fn resolve_glob_patterns(
78 root: &Path,
79 patterns: &[String],
80 exclusions: &[String],
81) -> Result<Vec<PathBuf>> {
82 let mut matched_paths = HashSet::new();
83
84 let mut inclusion_patterns = Vec::new();
86 let mut exclusion_patterns = Vec::new();
87
88 let default_ignores = [
90 "**/node_modules/**",
91 "**/.git/**",
92 "**/target/**",
93 "**/dist/**",
94 ];
95 for ignore in default_ignores {
96 if let Ok(pat) = Pattern::new(ignore) {
97 exclusion_patterns.push(pat);
98 }
99 }
100
101 for p in exclusions {
102 if let Ok(pat) = Pattern::new(p) {
103 exclusion_patterns.push(pat);
104 }
105 }
106
107 for p in patterns {
108 if let Some(stripped) = p.strip_prefix('!') {
109 if let Ok(pat) = Pattern::new(stripped) {
110 exclusion_patterns.push(pat);
111 }
112 } else if let Ok(pat) = Pattern::new(p) {
113 inclusion_patterns.push(pat);
114 }
115 }
116
117 let walker = WalkDir::new(root).follow_links(false);
119
120 for entry in walker
121 .into_iter()
122 .filter_entry(|e| {
123 let name = e.file_name().to_str().unwrap_or("");
124 if name == "node_modules" || name == ".git" || name == "target" || name == "dist" {
126 return false;
127 }
128 true
129 })
130 .filter_map(std::result::Result::ok)
131 {
132 if !entry.file_type().is_dir() {
133 continue;
134 }
135
136 let path = entry.path();
137 if path == root {
139 continue;
140 }
141
142 let Ok(rel_path) = path.strip_prefix(root) else {
144 continue;
145 };
146
147 let is_excluded = exclusion_patterns.iter().any(|p| p.matches_path(rel_path));
149 if is_excluded {
150 continue;
151 }
152
153 let is_included = inclusion_patterns.iter().any(|p| p.matches_path(rel_path));
155 if is_included {
156 matched_paths.insert(path.to_path_buf());
157 }
158 }
159
160 let mut result: Vec<PathBuf> = matched_paths.into_iter().collect();
161 result.sort();
162 Ok(result)
163}
164
165pub fn read_json_file<T: DeserializeOwned>(path: &Path) -> Result<T> {
171 let content = fs::read_to_string(path).map_err(|e| Error::Io {
172 source: e,
173 path: Some(path.to_path_buf()),
174 operation: "reading json file".to_string(),
175 })?;
176
177 serde_json::from_str(&content).map_err(|e| Error::Json {
178 source: e,
179 path: Some(path.to_path_buf()),
180 })
181}
182
183#[cfg(feature = "serde_yaml")]
189pub fn read_yaml_file<T: DeserializeOwned>(path: &Path) -> Result<T> {
190 let content = fs::read_to_string(path).map_err(|e| Error::Io {
191 source: e,
192 path: Some(path.to_path_buf()),
193 operation: "reading yaml file".to_string(),
194 })?;
195
196 serde_yaml::from_str(&content).map_err(|e| Error::Yaml {
197 source: e,
198 path: Some(path.to_path_buf()),
199 })
200}
201
202#[cfg(feature = "toml")]
208pub fn read_toml_file<T: DeserializeOwned>(path: &Path) -> Result<T> {
209 let content = fs::read_to_string(path).map_err(|e| Error::Io {
210 source: e,
211 path: Some(path.to_path_buf()),
212 operation: "reading toml file".to_string(),
213 })?;
214
215 toml::from_str(&content).map_err(|e| Error::Toml {
216 source: e,
217 path: Some(path.to_path_buf()),
218 })
219}