1use thiserror::Error;
2
3#[derive(Debug, Error)]
5pub enum UbtError {
6 #[error("Error: {tool} is not installed.{install_guidance}")]
7 ToolNotFound {
8 tool: String,
9 install_guidance: String,
10 },
11
12 #[error("\"{command}\" is not supported by the {plugin} plugin. {hint}")]
13 CommandUnsupported {
14 command: String,
15 plugin: String,
16 hint: String,
17 },
18
19 #[error(
20 "No command configured for \"{command}\". Add it to ubt.toml:\n\n [commands]\n \"{command}\" = \"your command here\""
21 )]
22 CommandUnmapped { command: String },
23
24 #[error("{message}")]
25 ConfigError { message: String },
26
27 #[error(
28 "Multiple plugins detected: {plugins}. Set tool in ubt.toml:\n\n [project]\n tool = \"{suggested_tool}\""
29 )]
30 PluginConflict {
31 plugins: String,
32 suggested_tool: String,
33 },
34
35 #[error("Could not detect project type. Run \"ubt init\" or create ubt.toml.")]
36 NoPluginMatch,
37
38 #[error("Failed to load plugin \"{name}\": {detail}")]
39 PluginLoadError { name: String, detail: String },
40
41 #[error("Template error: {0}")]
42 TemplateError(String),
43
44 #[error("Execution error: {0}")]
45 ExecutionError(String),
46
47 #[error("Alias \"{alias}\" conflicts with built-in command \"{command}\"")]
48 AliasConflict { alias: String, command: String },
49
50 #[error("Unknown command \"{name}\". Run \"ubt --help\" for available commands.")]
51 UnknownCommand { name: String },
52
53 #[error("Invalid glob pattern \"{pattern}\": {detail}")]
54 InvalidGlobPattern { pattern: String, detail: String },
55
56 #[error(transparent)]
57 Io(#[from] std::io::Error),
58}
59
60pub type Result<T> = std::result::Result<T, UbtError>;
62
63impl UbtError {
64 pub fn tool_not_found(tool: impl Into<String>, install_help: Option<&str>) -> Self {
66 let install_guidance = match install_help {
67 Some(help) => format!("\n\n{help}"),
68 None => String::new(),
69 };
70 UbtError::ToolNotFound {
71 tool: tool.into(),
72 install_guidance,
73 }
74 }
75
76 pub fn config_error(line: Option<usize>, detail: impl Into<String>) -> Self {
78 let detail = detail.into();
79 let message = match line {
80 Some(n) => format!("Error in ubt.toml [line {n}]: {detail}"),
81 None => format!("Error in ubt.toml: {detail}"),
82 };
83 UbtError::ConfigError { message }
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90
91 #[test]
92 fn tool_not_found_with_install_help() {
93 let err = UbtError::tool_not_found("npm", Some("Install via: https://nodejs.org"));
94 assert_eq!(
95 err.to_string(),
96 "Error: npm is not installed.\n\nInstall via: https://nodejs.org"
97 );
98 }
99
100 #[test]
101 fn tool_not_found_without_install_help() {
102 let err = UbtError::tool_not_found("cargo", None);
103 assert_eq!(err.to_string(), "Error: cargo is not installed.");
104 }
105
106 #[test]
107 fn command_unsupported_formats() {
108 let err = UbtError::CommandUnsupported {
109 command: "lint".into(),
110 plugin: "go".into(),
111 hint: "Try \"ubt run lint\" instead.".into(),
112 };
113 assert_eq!(
114 err.to_string(),
115 "\"lint\" is not supported by the go plugin. Try \"ubt run lint\" instead."
116 );
117 }
118
119 #[test]
120 fn command_unmapped_formats() {
121 let err = UbtError::CommandUnmapped {
122 command: "deploy".into(),
123 };
124 assert_eq!(
125 err.to_string(),
126 "No command configured for \"deploy\". Add it to ubt.toml:\n\n [commands]\n \"deploy\" = \"your command here\""
127 );
128 }
129
130 #[test]
131 fn config_error_with_line() {
132 let err = UbtError::config_error(Some(42), "invalid key");
133 assert_eq!(err.to_string(), "Error in ubt.toml [line 42]: invalid key");
134 }
135
136 #[test]
137 fn config_error_without_line() {
138 let err = UbtError::config_error(None, "missing section");
139 assert_eq!(err.to_string(), "Error in ubt.toml: missing section");
140 }
141
142 #[test]
143 fn plugin_conflict_formats() {
144 let err = UbtError::PluginConflict {
145 plugins: "node, bun".into(),
146 suggested_tool: "node".into(),
147 };
148 assert_eq!(
149 err.to_string(),
150 "Multiple plugins detected: node, bun. Set tool in ubt.toml:\n\n [project]\n tool = \"node\""
151 );
152 }
153
154 #[test]
155 fn no_plugin_match_formats() {
156 let err = UbtError::NoPluginMatch;
157 assert_eq!(
158 err.to_string(),
159 "Could not detect project type. Run \"ubt init\" or create ubt.toml."
160 );
161 }
162
163 #[test]
164 fn plugin_load_error_formats() {
165 let err = UbtError::PluginLoadError {
166 name: "rust".into(),
167 detail: "file not found".into(),
168 };
169 assert_eq!(
170 err.to_string(),
171 "Failed to load plugin \"rust\": file not found"
172 );
173 }
174
175 #[test]
176 fn template_error_formats() {
177 let err = UbtError::TemplateError("unresolved placeholder".into());
178 assert_eq!(err.to_string(), "Template error: unresolved placeholder");
179 }
180
181 #[test]
182 fn execution_error_formats() {
183 let err = UbtError::ExecutionError("process killed".into());
184 assert_eq!(err.to_string(), "Execution error: process killed");
185 }
186
187 #[test]
188 fn alias_conflict_formats() {
189 let err = UbtError::AliasConflict {
190 alias: "t".into(),
191 command: "test".into(),
192 };
193 assert_eq!(
194 err.to_string(),
195 "Alias \"t\" conflicts with built-in command \"test\""
196 );
197 }
198
199 #[test]
200 fn io_error_from_conversion() {
201 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "gone");
202 let ubt_err: UbtError = io_err.into();
203 assert!(matches!(ubt_err, UbtError::Io(_)));
204 assert_eq!(ubt_err.to_string(), "gone");
205 }
206
207 #[test]
208 fn error_is_send_and_sync() {
209 fn assert_send_sync<T: Send + Sync>() {}
210 assert_send_sync::<UbtError>();
211 }
212
213 #[test]
214 fn result_type_alias_works() {
215 fn returns_ok() -> Result<i32> {
216 Ok(42)
217 }
218 fn returns_err() -> Result<i32> {
219 Err(UbtError::NoPluginMatch)
220 }
221 assert_eq!(returns_ok().unwrap(), 42);
222 assert!(returns_err().is_err());
223 }
224}