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