1use cuengine::ModuleEvalOptions;
11use cuenv_core::ModuleEvaluation;
12use cuenv_core::Result;
13use cuenv_core::cue::discovery::{
14 adjust_meta_key_path, compute_relative_path, discover_env_cue_directories, format_eval_errors,
15};
16use std::collections::HashMap;
17use std::path::{Path, PathBuf};
18
19#[must_use]
21pub fn find_cue_module_root(start: &Path) -> Option<PathBuf> {
22 cuenv_core::cue::discovery::find_cue_module_root(start)
23}
24
25pub fn evaluate_module_from_cwd() -> Result<ModuleEvaluation> {
52 const PACKAGE: &str = "cuenv";
53
54 let cwd = std::env::current_dir().map_err(|e| cuenv_core::Error::Io {
55 source: e,
56 path: None,
57 operation: "get current directory".to_string(),
58 })?;
59
60 let module_root = find_cue_module_root(&cwd).ok_or_else(|| {
61 cuenv_core::Error::configuration(
62 "Not inside a CUE module. Run 'cue mod init' or navigate to a directory with cue.mod/",
63 )
64 })?;
65
66 let env_cue_dirs = discover_env_cue_directories(&module_root, PACKAGE);
68
69 if env_cue_dirs.is_empty() {
70 return Err(cuenv_core::Error::configuration(format!(
71 "No env.cue files with package '{PACKAGE}' found in module: {}",
72 module_root.display()
73 )));
74 }
75
76 let mut all_instances = HashMap::new();
78 let mut all_projects = Vec::new();
79 let mut all_meta = HashMap::new();
80 let mut eval_errors = Vec::new();
81
82 for dir in env_cue_dirs {
83 let dir_rel_path = compute_relative_path(&dir, &module_root);
84 let options = ModuleEvalOptions {
85 recursive: false,
86 with_references: true,
87 target_dir: Some(dir.to_string_lossy().to_string()),
88 ..Default::default()
89 };
90
91 match cuengine::evaluate_module(&module_root, PACKAGE, Some(&options)) {
92 Ok(raw) => {
93 for (path_str, value) in raw.instances {
95 let rel_path = if path_str == "." {
96 dir_rel_path.clone()
97 } else {
98 path_str
99 };
100 all_instances.insert(rel_path.clone(), value);
101 }
102
103 for project_path in raw.projects {
104 let rel_project_path = if project_path == "." {
105 dir_rel_path.clone()
106 } else {
107 project_path
108 };
109 if !all_projects.contains(&rel_project_path) {
110 all_projects.push(rel_project_path);
111 }
112 }
113
114 for (meta_key, meta_value) in raw.meta {
116 let adjusted_key = adjust_meta_key_path(&meta_key, &dir_rel_path);
117 all_meta.insert(adjusted_key, meta_value);
118 }
119 }
120 Err(e) => {
121 tracing::warn!(
122 dir = %dir.display(),
123 error = %e,
124 "Failed to evaluate env.cue - skipping directory"
125 );
126 eval_errors.push((dir, e));
127 }
128 }
129 }
130
131 if all_instances.is_empty() {
132 let error_summary = format_eval_errors(&eval_errors);
133 return Err(cuenv_core::Error::configuration(format!(
134 "No instances could be evaluated. All directories failed:\n{error_summary}"
135 )));
136 }
137
138 let references = if all_meta.is_empty() {
140 None
141 } else {
142 Some(
143 all_meta
144 .into_iter()
145 .filter_map(|(k, v)| v.reference.map(|r| (k, r)))
146 .collect(),
147 )
148 };
149
150 Ok(ModuleEvaluation::from_raw(
151 module_root,
152 all_instances,
153 all_projects,
154 references,
155 ))
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161
162 #[test]
167 fn test_find_cue_module_root_from_nested_dir() {
168 use std::fs;
169 use tempfile::TempDir;
170
171 let temp_dir = TempDir::new().expect("Failed to create temp dir");
172 let root = temp_dir.path();
173
174 fs::create_dir_all(root.join("cue.mod")).expect("Failed to create cue.mod");
176
177 let nested = root.join("services").join("api");
179 fs::create_dir_all(&nested).expect("Failed to create nested dir");
180
181 let found = find_cue_module_root(&nested);
183 assert!(found.is_some(), "Should find module root from nested dir");
184
185 let found_path = found.unwrap();
186 assert_eq!(
187 found_path.canonicalize().unwrap(),
188 root.canonicalize().unwrap(),
189 "Found root should match actual root"
190 );
191 }
192
193 #[test]
194 fn test_find_cue_module_root_not_found() {
195 use tempfile::TempDir;
196
197 let temp_dir = TempDir::new().expect("Failed to create temp dir");
198 let found = find_cue_module_root(temp_dir.path());
201 assert!(
202 found.is_none(),
203 "Should not find module root without cue.mod"
204 );
205 }
206
207 #[test]
208 fn test_find_cue_module_root_from_root() {
209 use std::fs;
210 use tempfile::TempDir;
211
212 let temp_dir = TempDir::new().expect("Failed to create temp dir");
213 let root = temp_dir.path();
214
215 fs::create_dir_all(root.join("cue.mod")).expect("Failed to create cue.mod");
217
218 let found = find_cue_module_root(root);
220 assert!(found.is_some());
221 }
222
223 #[test]
224 fn test_find_cue_module_root_deeply_nested() {
225 use std::fs;
226 use tempfile::TempDir;
227
228 let temp_dir = TempDir::new().expect("Failed to create temp dir");
229 let root = temp_dir.path();
230
231 fs::create_dir_all(root.join("cue.mod")).expect("Failed to create cue.mod");
233
234 let nested = root.join("a").join("b").join("c").join("d").join("e");
236 fs::create_dir_all(&nested).expect("Failed to create nested dir");
237
238 let found = find_cue_module_root(&nested);
240 assert!(found.is_some());
241 assert_eq!(
242 found.unwrap().canonicalize().unwrap(),
243 root.canonicalize().unwrap()
244 );
245 }
246
247 #[test]
248 fn test_find_cue_module_root_cue_mod_file_not_dir() {
249 use std::fs;
250 use tempfile::TempDir;
251
252 let temp_dir = TempDir::new().expect("Failed to create temp dir");
253 let root = temp_dir.path();
254
255 fs::write(root.join("cue.mod"), "not a directory").expect("Failed to create file");
257
258 let found = find_cue_module_root(root);
259 assert!(
260 found.is_none(),
261 "File named cue.mod should not count as module root"
262 );
263 }
264
265 #[test]
266 fn test_find_cue_module_root_intermediate_cue_mod() {
267 use std::fs;
268 use tempfile::TempDir;
269
270 let temp_dir = TempDir::new().expect("Failed to create temp dir");
271 let root = temp_dir.path();
272
273 let intermediate = root.join("project");
275 fs::create_dir_all(intermediate.join("cue.mod")).expect("Failed to create cue.mod");
276
277 let nested = intermediate.join("services").join("api");
279 fs::create_dir_all(&nested).expect("Failed to create nested dir");
280
281 let found = find_cue_module_root(&nested);
283 assert!(found.is_some());
284 assert_eq!(
285 found.unwrap().canonicalize().unwrap(),
286 intermediate.canonicalize().unwrap()
287 );
288 }
289
290 #[test]
291 fn test_find_cue_module_root_nonexistent_path() {
292 let path = PathBuf::from("/nonexistent/path/that/does/not/exist");
293 let found = find_cue_module_root(&path);
294 assert!(found.is_none());
295 }
296}