1use miette::Diagnostic;
4use std::path::PathBuf;
5use thiserror::Error;
6
7pub type Result<T> = std::result::Result<T, Error>;
9
10#[derive(Error, Debug, Diagnostic)]
12pub enum Error {
13 #[error("Changeset I/O error: {message}")]
15 #[diagnostic(
16 code(cuenv::release::changeset_io),
17 help("Check that the .cuenv/changesets directory exists and is writable")
18 )]
19 ChangesetIo {
20 message: String,
22 path: Option<PathBuf>,
24 #[source]
26 source: Option<std::io::Error>,
27 },
28
29 #[error("Invalid changeset format: {message}")]
31 #[diagnostic(
32 code(cuenv::release::changeset_parse),
33 help("Ensure the changeset file is valid Markdown with proper frontmatter")
34 )]
35 ChangesetParse {
36 message: String,
38 path: Option<PathBuf>,
40 },
41
42 #[error("Invalid version: {version}")]
44 #[diagnostic(
45 code(cuenv::release::invalid_version),
46 help("Version must follow semantic versioning (e.g., 1.0.0, 2.1.0-beta.1)")
47 )]
48 InvalidVersion {
49 version: String,
51 },
52
53 #[error("Package not found: {name}")]
55 #[diagnostic(
56 code(cuenv::release::package_not_found),
57 help("Ensure the package exists in the workspace and is properly configured")
58 )]
59 PackageNotFound {
60 name: String,
62 },
63
64 #[error("No changesets found")]
66 #[diagnostic(
67 code(cuenv::release::no_changesets),
68 help("Create changesets with 'cuenv changeset add' before running release version")
69 )]
70 NoChangesets,
71
72 #[error("Release configuration error: {message}")]
74 #[diagnostic(code(cuenv::release::config), help("{help}"))]
75 Config {
76 message: String,
78 help: String,
80 },
81
82 #[error("Manifest error: {message}")]
84 #[diagnostic(
85 code(cuenv::release::manifest),
86 help("Check that the manifest file exists and is properly formatted")
87 )]
88 Manifest {
89 message: String,
91 path: Option<PathBuf>,
93 },
94
95 #[error("Git error: {message}")]
97 #[diagnostic(
98 code(cuenv::release::git),
99 help("Ensure you are in a git repository and have the necessary permissions")
100 )]
101 Git {
102 message: String,
104 },
105
106 #[error("Publish failed: {message}")]
108 #[diagnostic(code(cuenv::release::publish))]
109 Publish {
110 message: String,
112 package: Option<String>,
114 },
115
116 #[error("Artifact error: {message}")]
118 #[diagnostic(
119 code(cuenv::release::artifact),
120 help("Check that the binary exists and is readable")
121 )]
122 Artifact {
123 message: String,
125 path: Option<PathBuf>,
127 },
128
129 #[error("{backend} backend error: {message}")]
131 #[diagnostic(code(cuenv::release::backend))]
132 Backend {
133 backend: String,
135 message: String,
137 help: Option<String>,
139 },
140
141 #[error("I/O error: {0}")]
143 #[diagnostic(code(cuenv::release::io))]
144 Io(#[from] std::io::Error),
145
146 #[error("JSON error: {0}")]
148 #[diagnostic(code(cuenv::release::json))]
149 Json(#[from] serde_json::Error),
150
151 #[error("TOML parse error: {0}")]
153 #[diagnostic(code(cuenv::release::toml_parse))]
154 TomlParse(#[from] toml::de::Error),
155
156 #[error("TOML serialization error: {0}")]
158 #[diagnostic(code(cuenv::release::toml_ser))]
159 TomlSer(#[from] toml::ser::Error),
160}
161
162impl Error {
163 #[must_use]
165 pub fn changeset_io(message: impl Into<String>, path: Option<PathBuf>) -> Self {
166 Self::ChangesetIo {
167 message: message.into(),
168 path,
169 source: None,
170 }
171 }
172
173 #[must_use]
175 pub fn changeset_io_with_source(
176 message: impl Into<String>,
177 path: Option<PathBuf>,
178 source: std::io::Error,
179 ) -> Self {
180 Self::ChangesetIo {
181 message: message.into(),
182 path,
183 source: Some(source),
184 }
185 }
186
187 #[must_use]
189 pub fn changeset_parse(message: impl Into<String>, path: Option<PathBuf>) -> Self {
190 Self::ChangesetParse {
191 message: message.into(),
192 path,
193 }
194 }
195
196 #[must_use]
198 pub fn invalid_version(version: impl Into<String>) -> Self {
199 Self::InvalidVersion {
200 version: version.into(),
201 }
202 }
203
204 #[must_use]
206 pub fn package_not_found(name: impl Into<String>) -> Self {
207 Self::PackageNotFound { name: name.into() }
208 }
209
210 #[must_use]
212 pub fn config(message: impl Into<String>, help: impl Into<String>) -> Self {
213 Self::Config {
214 message: message.into(),
215 help: help.into(),
216 }
217 }
218
219 #[must_use]
221 pub fn manifest(message: impl Into<String>, path: Option<PathBuf>) -> Self {
222 Self::Manifest {
223 message: message.into(),
224 path,
225 }
226 }
227
228 #[must_use]
230 pub fn git(message: impl Into<String>) -> Self {
231 Self::Git {
232 message: message.into(),
233 }
234 }
235
236 #[must_use]
238 pub fn publish(message: impl Into<String>, package: Option<String>) -> Self {
239 Self::Publish {
240 message: message.into(),
241 package,
242 }
243 }
244
245 #[must_use]
247 pub fn artifact(message: impl Into<String>, path: Option<PathBuf>) -> Self {
248 Self::Artifact {
249 message: message.into(),
250 path,
251 }
252 }
253
254 #[must_use]
256 pub fn backend(
257 backend: impl Into<String>,
258 message: impl Into<String>,
259 help: Option<String>,
260 ) -> Self {
261 Self::Backend {
262 backend: backend.into(),
263 message: message.into(),
264 help,
265 }
266 }
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272
273 #[test]
274 fn test_changeset_io_error() {
275 let err = Error::changeset_io("failed to write", Some(PathBuf::from(".cuenv/test.md")));
276 assert!(err.to_string().contains("Changeset I/O error"));
277 }
278
279 #[test]
280 fn test_changeset_io_error_no_path() {
281 let err = Error::changeset_io("failed to write", None);
282 assert!(err.to_string().contains("Changeset I/O error"));
283 }
284
285 #[test]
286 fn test_changeset_io_with_source() {
287 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
288 let err = Error::changeset_io_with_source(
289 "failed to read",
290 Some(PathBuf::from("test.md")),
291 io_err,
292 );
293 assert!(err.to_string().contains("Changeset I/O error"));
294 }
295
296 #[test]
297 fn test_changeset_parse_error() {
298 let err = Error::changeset_parse("invalid frontmatter", None);
299 assert!(err.to_string().contains("Invalid changeset format"));
300 }
301
302 #[test]
303 fn test_changeset_parse_error_with_path() {
304 let err = Error::changeset_parse("bad yaml", Some(PathBuf::from("changes.md")));
305 assert!(err.to_string().contains("Invalid changeset format"));
306 }
307
308 #[test]
309 fn test_invalid_version_error() {
310 let err = Error::invalid_version("not-a-version");
311 assert!(err.to_string().contains("not-a-version"));
312 }
313
314 #[test]
315 fn test_package_not_found_error() {
316 let err = Error::package_not_found("missing-pkg");
317 assert!(err.to_string().contains("missing-pkg"));
318 }
319
320 #[test]
321 fn test_config_error() {
322 let err = Error::config("bad config", "check your settings");
323 assert!(err.to_string().contains("bad config"));
324 }
325
326 #[test]
327 fn test_manifest_error() {
328 let err = Error::manifest("invalid toml", Some(PathBuf::from("Cargo.toml")));
329 assert!(err.to_string().contains("Manifest error"));
330 }
331
332 #[test]
333 fn test_manifest_error_no_path() {
334 let err = Error::manifest("missing field", None);
335 assert!(err.to_string().contains("Manifest error"));
336 }
337
338 #[test]
339 fn test_git_error() {
340 let err = Error::git("not a repository");
341 assert!(err.to_string().contains("Git error"));
342 }
343
344 #[test]
345 fn test_publish_error() {
346 let err = Error::publish("auth failed", Some("my-pkg".to_string()));
347 assert!(err.to_string().contains("Publish failed"));
348 }
349
350 #[test]
351 fn test_publish_error_no_package() {
352 let err = Error::publish("network error", None);
353 assert!(err.to_string().contains("Publish failed"));
354 }
355
356 #[test]
357 fn test_no_changesets_error() {
358 let err = Error::NoChangesets;
359 assert!(err.to_string().contains("No changesets found"));
360 }
361
362 #[test]
363 fn test_artifact_error() {
364 let err = Error::artifact(
365 "binary not found",
366 Some(PathBuf::from("target/release/bin")),
367 );
368 assert!(err.to_string().contains("Artifact error"));
369 }
370
371 #[test]
372 fn test_artifact_error_no_path() {
373 let err = Error::artifact("compression failed", None);
374 assert!(err.to_string().contains("Artifact error"));
375 }
376
377 #[test]
378 fn test_backend_error() {
379 let err = Error::backend("GitHub", "rate limited", Some("wait 1 hour".to_string()));
380 assert!(err.to_string().contains("GitHub"));
381 assert!(err.to_string().contains("rate limited"));
382 }
383
384 #[test]
385 fn test_backend_error_no_help() {
386 let err = Error::backend("Homebrew", "push failed", None);
387 assert!(err.to_string().contains("Homebrew"));
388 }
389
390 #[test]
391 fn test_from_io_error() {
392 let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
393 let err: Error = io_err.into();
394 assert!(err.to_string().contains("I/O error"));
395 }
396
397 #[test]
398 fn test_error_debug() {
399 let err = Error::NoChangesets;
400 let debug = format!("{err:?}");
401 assert!(debug.contains("NoChangesets"));
402 }
403}