cuenv-core 0.40.6

Core types and error handling for the cuenv ecosystem
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
//! Independent .rules.cue file discovery.
//!
//! Discovers `.rules.cue` files throughout the repository and evaluates
//! each one independently (NOT as part of module unification).

use ignore::WalkBuilder;
use std::path::{Path, PathBuf};

use crate::Result;
use crate::manifest::DirectoryRules;

/// A discovered .rules.cue configuration.
#[derive(Debug, Clone)]
pub struct DiscoveredRules {
    /// Path to the .rules.cue file.
    pub file_path: PathBuf,

    /// Directory containing the .rules.cue file.
    /// This is where ignore files and .editorconfig will be generated.
    pub directory: PathBuf,

    /// The parsed rules configuration.
    pub config: DirectoryRules,
}

/// Function type for evaluating a single .rules.cue file independently.
pub type RulesEvalFn = Box<dyn Fn(&Path) -> Result<DirectoryRules> + Send + Sync>;

/// Discovers .rules.cue files across the repository.
///
/// Each .rules.cue file is evaluated independently (not unified with
/// other CUE files in the module).
pub struct RulesDiscovery {
    /// Root directory to search from.
    root: PathBuf,

    /// Discovered rules configurations.
    discovered: Vec<DiscoveredRules>,

    /// Function to evaluate .rules.cue files.
    eval_fn: Option<RulesEvalFn>,
}

impl RulesDiscovery {
    /// Create a new RulesDiscovery for the given root directory.
    #[must_use]
    pub fn new(root: PathBuf) -> Self {
        Self {
            root,
            discovered: Vec::new(),
            eval_fn: None,
        }
    }

    /// Set the evaluation function for .rules.cue files.
    #[must_use]
    pub fn with_eval_fn(mut self, eval_fn: RulesEvalFn) -> Self {
        self.eval_fn = Some(eval_fn);
        self
    }

    /// Discover all .rules.cue files in the repository.
    ///
    /// # Errors
    ///
    /// Returns an error if no evaluation function is provided.
    pub fn discover(&mut self) -> std::result::Result<(), RulesDiscoveryError> {
        self.discovered.clear();

        let eval_fn = self
            .eval_fn
            .as_ref()
            .ok_or(RulesDiscoveryError::NoEvalFunction)?;

        let walker = WalkBuilder::new(&self.root)
            .follow_links(true)
            .standard_filters(true)
            .build();

        let mut load_failures = Vec::new();

        for result in walker {
            match result {
                Ok(entry) => {
                    let path = entry.path();
                    // Look for .rules.cue files
                    if path.file_name() == Some(".rules.cue".as_ref()) {
                        match Self::load_rules(path, eval_fn) {
                            Ok(rules) => self.discovered.push(rules),
                            Err(e) => {
                                tracing::warn!(
                                    path = %path.display(),
                                    error = %e,
                                    "Failed to load .rules.cue - skipping"
                                );
                                load_failures.push((path.to_path_buf(), e));
                            }
                        }
                    }
                }
                Err(e) => {
                    tracing::warn!(error = %e, "Error during directory scan");
                }
            }
        }

        if !load_failures.is_empty() {
            tracing::warn!(
                count = load_failures.len(),
                "Some .rules.cue files failed to load. \
                 Run with RUST_LOG=debug for details."
            );
        }

        tracing::debug!(
            discovered = self.discovered.len(),
            failures = load_failures.len(),
            "Rules discovery complete"
        );

        Ok(())
    }

    /// Load a single .rules.cue configuration.
    fn load_rules(
        file_path: &Path,
        eval_fn: &RulesEvalFn,
    ) -> std::result::Result<DiscoveredRules, RulesDiscoveryError> {
        let directory = file_path
            .parent()
            .ok_or_else(|| RulesDiscoveryError::InvalidPath(file_path.to_path_buf()))?
            .to_path_buf();

        // Evaluate the .rules.cue file independently
        let config = eval_fn(file_path)
            .map_err(|e| RulesDiscoveryError::EvalError(file_path.to_path_buf(), Box::new(e)))?;

        Ok(DiscoveredRules {
            file_path: file_path.to_path_buf(),
            directory,
            config,
        })
    }

    /// Get all discovered .rules.cue configurations.
    pub fn discovered(&self) -> &[DiscoveredRules] {
        &self.discovered
    }

    /// Get the root directory being searched.
    pub fn root(&self) -> &Path {
        &self.root
    }
}

/// Errors that can occur during rules discovery.
#[derive(Debug, thiserror::Error)]
pub enum RulesDiscoveryError {
    /// Invalid path encountered.
    #[error("Invalid path: {0}")]
    InvalidPath(PathBuf),

    /// Failed to evaluate a .rules.cue file.
    #[error("Failed to evaluate {}: {}", .0.display(), .1)]
    EvalError(PathBuf, #[source] Box<crate::Error>),

    /// No evaluation function was provided.
    #[error("No evaluation function provided")]
    NoEvalFunction,

    /// IO error during discovery.
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::manifest::DirectoryRules;
    use std::fs;
    use tempfile::TempDir;

    // ==========================================================================
    // RulesDiscovery construction tests
    // ==========================================================================

    #[test]
    fn test_rules_discovery_new() {
        let discovery = RulesDiscovery::new(PathBuf::from("/some/root"));

        assert_eq!(discovery.root(), Path::new("/some/root"));
        assert!(discovery.discovered().is_empty());
    }

    #[test]
    fn test_rules_discovery_with_eval_fn() {
        let eval_fn: RulesEvalFn = Box::new(|_| Ok(DirectoryRules::default()));
        let discovery = RulesDiscovery::new(PathBuf::from("/root")).with_eval_fn(eval_fn);

        // Can't directly check if eval_fn is set, but we can verify discover doesn't fail
        // with NoEvalFunction error (we'll test that separately)
        assert_eq!(discovery.root(), Path::new("/root"));
    }

    // ==========================================================================
    // RulesDiscovery::discover tests
    // ==========================================================================

    #[test]
    fn test_discover_no_eval_function_error() {
        let temp_dir = TempDir::new().unwrap();
        let mut discovery = RulesDiscovery::new(temp_dir.path().to_path_buf());

        let result = discovery.discover();

        assert!(result.is_err());
        let err = result.unwrap_err();
        assert!(matches!(err, RulesDiscoveryError::NoEvalFunction));
        assert_eq!(err.to_string(), "No evaluation function provided");
    }

    #[test]
    fn test_discover_empty_directory() {
        let temp_dir = TempDir::new().unwrap();
        let eval_fn: RulesEvalFn = Box::new(|_| Ok(DirectoryRules::default()));
        let mut discovery =
            RulesDiscovery::new(temp_dir.path().to_path_buf()).with_eval_fn(eval_fn);

        let result = discovery.discover();

        assert!(result.is_ok());
        assert!(discovery.discovered().is_empty());
    }

    #[test]
    fn test_discover_processes_walker_results() {
        // Test the basic discovery flow without relying on hidden file behavior
        let temp_dir = TempDir::new().unwrap();
        let root = temp_dir.path();

        // Discovery with no .rules.cue files should succeed with empty results
        let eval_fn: RulesEvalFn = Box::new(|_| Ok(DirectoryRules::default()));
        let mut discovery = RulesDiscovery::new(root.to_path_buf()).with_eval_fn(eval_fn);

        let result = discovery.discover();

        assert!(result.is_ok());
        // Empty because no .rules.cue files exist
        assert!(discovery.discovered().is_empty());
    }

    #[test]
    fn test_discover_ignores_non_rules_cue_files() {
        let temp_dir = TempDir::new().unwrap();
        let root = temp_dir.path();

        // Create various files that should NOT be picked up
        fs::write(root.join("rules.cue"), "").unwrap(); // Missing leading dot
        fs::write(root.join(".rules.txt"), "").unwrap(); // Wrong extension
        fs::write(root.join("config.cue"), "").unwrap(); // Different name

        let eval_fn: RulesEvalFn = Box::new(|_| Ok(DirectoryRules::default()));
        let mut discovery = RulesDiscovery::new(root.to_path_buf()).with_eval_fn(eval_fn);

        let result = discovery.discover();

        assert!(result.is_ok());
        assert!(discovery.discovered().is_empty());
    }

    #[test]
    fn test_discover_eval_failure_continues() {
        let temp_dir = TempDir::new().unwrap();
        let root = temp_dir.path();

        // Create two .rules.cue files
        fs::write(root.join(".rules.cue"), "").unwrap();
        fs::create_dir_all(root.join("subdir")).unwrap();
        fs::write(root.join("subdir/.rules.cue"), "").unwrap();

        // Eval function that always fails - discovery should still succeed
        // (failures are logged but not fatal)
        let eval_fn: RulesEvalFn = Box::new(|_path| Err(crate::Error::configuration("test error")));
        let mut discovery = RulesDiscovery::new(root.to_path_buf()).with_eval_fn(eval_fn);

        let result = discovery.discover();

        // Should succeed overall, even with eval failures
        assert!(result.is_ok());
        // No discoveries because all evaluations failed
        assert!(discovery.discovered().is_empty());
    }

    #[test]
    fn test_discover_clears_previous_results() {
        let temp_dir = TempDir::new().unwrap();
        let root = temp_dir.path();

        let eval_fn: RulesEvalFn = Box::new(|_| Ok(DirectoryRules::default()));
        let mut discovery = RulesDiscovery::new(root.to_path_buf()).with_eval_fn(eval_fn);

        // First discovery
        discovery.discover().unwrap();
        let first_count = discovery.discovered().len();

        // Second discovery - should start fresh (both empty)
        discovery.discover().unwrap();
        let second_count = discovery.discovered().len();

        // Both should be empty (no .rules.cue files)
        assert_eq!(first_count, second_count);
        assert_eq!(first_count, 0);
    }

    // ==========================================================================
    // DiscoveredRules tests
    // ==========================================================================

    #[test]
    fn test_discovered_rules_fields() {
        let discovered = DiscoveredRules {
            file_path: PathBuf::from("/repo/frontend/.rules.cue"),
            directory: PathBuf::from("/repo/frontend"),
            config: DirectoryRules::default(),
        };

        assert_eq!(
            discovered.file_path,
            PathBuf::from("/repo/frontend/.rules.cue")
        );
        assert_eq!(discovered.directory, PathBuf::from("/repo/frontend"));
    }

    #[test]
    fn test_discovered_rules_clone() {
        let discovered = DiscoveredRules {
            file_path: PathBuf::from("/repo/.rules.cue"),
            directory: PathBuf::from("/repo"),
            config: DirectoryRules::default(),
        };

        let cloned = discovered.clone();

        assert_eq!(cloned.file_path, discovered.file_path);
        assert_eq!(cloned.directory, discovered.directory);
    }

    // ==========================================================================
    // RulesDiscoveryError tests
    // ==========================================================================

    #[test]
    fn test_rules_discovery_error_invalid_path_display() {
        let err = RulesDiscoveryError::InvalidPath(PathBuf::from("/bad/path"));
        assert_eq!(err.to_string(), "Invalid path: /bad/path");
    }

    #[test]
    fn test_rules_discovery_error_no_eval_function_display() {
        let err = RulesDiscoveryError::NoEvalFunction;
        assert_eq!(err.to_string(), "No evaluation function provided");
    }

    #[test]
    fn test_rules_discovery_error_io_display() {
        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
        let err = RulesDiscoveryError::Io(io_err);
        assert!(err.to_string().contains("file not found"));
    }

    #[test]
    fn test_rules_discovery_error_eval_error_display() {
        let inner_err = crate::Error::configuration("CUE syntax error");
        let err =
            RulesDiscoveryError::EvalError(PathBuf::from("/repo/.rules.cue"), Box::new(inner_err));
        let display = err.to_string();
        assert!(display.contains("/repo/.rules.cue"));
        assert!(display.contains("CUE syntax error"));
    }

    #[test]
    fn test_rules_discovery_error_io_from() {
        let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
        let err: RulesDiscoveryError = io_err.into();
        assert!(matches!(err, RulesDiscoveryError::Io(_)));
    }

    // ==========================================================================
    // load_rules tests (unit testing the internal function)
    // ==========================================================================

    #[test]
    fn test_load_rules_sets_correct_directory() {
        // Test load_rules directly by creating a temporary file and calling it
        let temp_dir = TempDir::new().unwrap();
        let root = temp_dir.path();
        let subdir = root.join("frontend").join("components");
        fs::create_dir_all(&subdir).unwrap();
        let rules_file = subdir.join(".rules.cue");
        fs::write(&rules_file, "").unwrap();

        let eval_fn: RulesEvalFn = Box::new(|_| Ok(DirectoryRules::default()));

        // Call load_rules directly
        let result = RulesDiscovery::load_rules(&rules_file, &eval_fn);

        assert!(result.is_ok());
        let discovered = result.unwrap();
        assert_eq!(discovered.directory, subdir);
        assert_eq!(discovered.file_path, rules_file);
    }

    #[test]
    fn test_load_rules_eval_fn_error() {
        let temp_dir = TempDir::new().unwrap();
        let rules_file = temp_dir.path().join(".rules.cue");
        fs::write(&rules_file, "").unwrap();

        let eval_fn: RulesEvalFn = Box::new(|_| Err(crate::Error::configuration("parse failed")));

        let result = RulesDiscovery::load_rules(&rules_file, &eval_fn);

        assert!(result.is_err());
        let err = result.unwrap_err();
        assert!(matches!(err, RulesDiscoveryError::EvalError(_, _)));
    }

    #[test]
    fn test_root_accessor() {
        let path = PathBuf::from("/custom/root/path");
        let discovery = RulesDiscovery::new(path.clone());

        assert_eq!(discovery.root(), path.as_path());
    }

    #[test]
    fn test_discovered_accessor_empty() {
        let discovery = RulesDiscovery::new(PathBuf::from("/root"));

        assert!(discovery.discovered().is_empty());
    }
}