1use std::io;
4
5use thiserror::Error;
6
7pub type DomainResult<T> = Result<T, DomainError>;
9
10#[derive(Debug, Error)]
12pub enum DomainError {
13 #[error("I/O failure while {context}: {source}")]
15 Io {
16 context: &'static str,
18 #[source]
20 source: io::Error,
21 },
22
23 #[error("{entity} not found: {id}")]
25 NotFound {
26 entity: &'static str,
28 id: String,
30 },
31
32 #[error("Ambiguous {entity} target '{input}'. Matches: {matches}")]
34 AmbiguousTarget {
35 entity: &'static str,
37 input: String,
39 matches: String,
41 },
42}
43
44impl DomainError {
45 pub fn io(context: &'static str, source: io::Error) -> Self {
47 Self::Io { context, source }
48 }
49
50 pub fn not_found(entity: &'static str, id: impl Into<String>) -> Self {
52 Self::NotFound {
53 entity,
54 id: id.into(),
55 }
56 }
57
58 pub fn ambiguous_target(entity: &'static str, input: &str, matches: &[String]) -> Self {
60 Self::AmbiguousTarget {
61 entity,
62 input: input.to_string(),
63 matches: matches.join(", "),
64 }
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71
72 #[test]
73 fn io_constructor_preserves_context_and_source() {
74 let source = io::Error::new(io::ErrorKind::PermissionDenied, "no access");
75 let error = DomainError::io("reading tasks", source);
76
77 match error {
78 DomainError::Io { context, source } => {
79 assert_eq!(context, "reading tasks");
80 assert_eq!(source.kind(), io::ErrorKind::PermissionDenied);
81 assert_eq!(source.to_string(), "no access");
82 }
83 other => panic!("expected io variant, got {other:?}"),
84 }
85 }
86
87 #[test]
88 fn not_found_constructor_formats_display_message() {
89 let error = DomainError::not_found("module", "123_core");
90
91 match &error {
92 DomainError::NotFound { entity, id } => {
93 assert_eq!(*entity, "module");
94 assert_eq!(id, "123_core");
95 }
96 other => panic!("expected not found variant, got {other:?}"),
97 }
98
99 assert_eq!(error.to_string(), "module not found: 123_core");
100 }
101
102 #[test]
103 fn ambiguous_target_joins_candidates_in_display_message() {
104 let matches = vec!["001-01_alpha".to_string(), "001-02_alpha-fix".to_string()];
105 let error = DomainError::ambiguous_target("change", "alpha", &matches);
106
107 match &error {
108 DomainError::AmbiguousTarget {
109 entity,
110 input,
111 matches,
112 } => {
113 assert_eq!(*entity, "change");
114 assert_eq!(input, "alpha");
115 assert_eq!(matches, "001-01_alpha, 001-02_alpha-fix");
116 }
117 other => panic!("expected ambiguous target variant, got {other:?}"),
118 }
119
120 assert_eq!(
121 error.to_string(),
122 "Ambiguous change target 'alpha'. Matches: 001-01_alpha, 001-02_alpha-fix"
123 );
124 }
125}