1use miette::Diagnostic;
4use std::path::PathBuf;
5use thiserror::Error;
6
7#[derive(Debug, Error, Diagnostic)]
9pub enum FlakeLockError {
10 #[error("Failed to parse flake.lock: {0}")]
12 #[diagnostic(
13 code(cuenv::ci::flake::parse),
14 help("Ensure flake.lock is valid JSON and follows Nix flake.lock schema v7")
15 )]
16 ParseError(String),
17
18 #[error("Failed to read flake.lock at {path}: {message}")]
20 #[diagnostic(
21 code(cuenv::ci::flake::io),
22 help("Check that flake.lock exists and is readable")
23 )]
24 IoError {
25 path: PathBuf,
27 message: String,
29 },
30
31 #[error("Flake purity check failed with {count} unlocked input(s): {}", inputs.join(", "))]
33 #[diagnostic(
34 code(cuenv::ci::flake::impure_strict),
35 help(
36 "In strict mode, all flake inputs must be locked. Run 'nix flake lock' to fix, or use purity_mode: warning/override"
37 )
38 )]
39 StrictModeViolation {
40 count: usize,
42 inputs: Vec<String>,
44 },
45
46 #[error("No flake.lock file found at {path}")]
48 #[diagnostic(
49 code(cuenv::ci::flake::missing),
50 help("Run 'nix flake lock' to generate a flake.lock file")
51 )]
52 MissingLockFile {
53 path: PathBuf,
55 },
56}
57
58impl FlakeLockError {
59 #[must_use]
61 pub fn parse(message: impl Into<String>) -> Self {
62 Self::ParseError(message.into())
63 }
64
65 #[must_use]
67 pub fn io(path: impl Into<PathBuf>, message: impl Into<String>) -> Self {
68 Self::IoError {
69 path: path.into(),
70 message: message.into(),
71 }
72 }
73
74 #[must_use]
76 pub const fn strict_violation(inputs: Vec<String>) -> Self {
77 Self::StrictModeViolation {
78 count: inputs.len(),
79 inputs,
80 }
81 }
82
83 #[must_use]
85 pub fn missing(path: impl Into<PathBuf>) -> Self {
86 Self::MissingLockFile { path: path.into() }
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93
94 #[test]
95 fn test_parse_error_constructor() {
96 let err = FlakeLockError::parse("invalid JSON");
97 assert!(matches!(err, FlakeLockError::ParseError(_)));
98 let display = format!("{err}");
99 assert!(display.contains("Failed to parse flake.lock"));
100 assert!(display.contains("invalid JSON"));
101 }
102
103 #[test]
104 fn test_io_error_constructor() {
105 let err = FlakeLockError::io("/path/to/flake.lock", "permission denied");
106 if let FlakeLockError::IoError { path, message } = err {
107 assert_eq!(path, PathBuf::from("/path/to/flake.lock"));
108 assert_eq!(message, "permission denied");
109 } else {
110 panic!("Expected IoError");
111 }
112 }
113
114 #[test]
115 fn test_io_error_display() {
116 let err = FlakeLockError::io("/project/flake.lock", "file not found");
117 let display = format!("{err}");
118 assert!(display.contains("/project/flake.lock"));
119 assert!(display.contains("file not found"));
120 }
121
122 #[test]
123 fn test_strict_violation_constructor() {
124 let inputs = vec!["nixpkgs".to_string(), "home-manager".to_string()];
125 let err = FlakeLockError::strict_violation(inputs);
126
127 if let FlakeLockError::StrictModeViolation { count, inputs } = err {
128 assert_eq!(count, 2);
129 assert!(inputs.contains(&"nixpkgs".to_string()));
130 assert!(inputs.contains(&"home-manager".to_string()));
131 } else {
132 panic!("Expected StrictModeViolation");
133 }
134 }
135
136 #[test]
137 fn test_strict_violation_display() {
138 let inputs = vec!["input1".to_string(), "input2".to_string()];
139 let err = FlakeLockError::strict_violation(inputs);
140 let display = format!("{err}");
141 assert!(display.contains("2 unlocked input(s)"));
142 assert!(display.contains("input1"));
143 assert!(display.contains("input2"));
144 }
145
146 #[test]
147 fn test_missing_lock_file_constructor() {
148 let err = FlakeLockError::missing("/project/flake.lock");
149 if let FlakeLockError::MissingLockFile { path } = err {
150 assert_eq!(path, PathBuf::from("/project/flake.lock"));
151 } else {
152 panic!("Expected MissingLockFile");
153 }
154 }
155
156 #[test]
157 fn test_missing_lock_file_display() {
158 let err = FlakeLockError::missing("/my/project/flake.lock");
159 let display = format!("{err}");
160 assert!(display.contains("No flake.lock file found"));
161 assert!(display.contains("/my/project/flake.lock"));
162 }
163
164 #[test]
165 fn test_error_debug() {
166 let err = FlakeLockError::parse("test error");
167 let debug_str = format!("{err:?}");
168 assert!(debug_str.contains("ParseError"));
169 }
170
171 #[test]
172 fn test_strict_violation_empty_inputs() {
173 let err = FlakeLockError::strict_violation(vec![]);
174 if let FlakeLockError::StrictModeViolation { count, inputs } = err {
175 assert_eq!(count, 0);
176 assert!(inputs.is_empty());
177 } else {
178 panic!("Expected StrictModeViolation");
179 }
180 }
181}