theater_cli/
error.rs

1use std::net::SocketAddr;
2use thiserror::Error;
3
4/// Main error type for the Theater CLI
5#[derive(Error, Debug)]
6pub enum CliError {
7    /// Connection-related errors
8    #[error("Failed to connect to Theater server at {address}")]
9    ConnectionFailed {
10        address: SocketAddr,
11        #[source]
12        source: anyhow::Error,
13    },
14
15    #[error("Connection lost to Theater server")]
16    ConnectionLost,
17
18    #[error("Connection timeout after {timeout}s")]
19    ConnectionTimeout { timeout: u64 },
20
21    /// Actor-related errors
22    #[error("Actor '{actor_id}' not found")]
23    ActorNotFound { actor_id: String },
24
25    #[error("Actor '{actor_id}' failed to start: {reason}")]
26    ActorStartFailed { actor_id: String, reason: String },
27
28    #[error("Actor '{actor_id}' is not running")]
29    ActorNotRunning { actor_id: String },
30
31    /// Project and build errors
32    #[error("Invalid project directory: {path}")]
33    InvalidProjectDirectory { path: String },
34
35    #[error("Build failed: {output}")]
36    BuildFailed { output: String },
37
38    #[error("Missing required tool: {tool}. Please install it with: {install_command}")]
39    MissingTool {
40        tool: String,
41        install_command: String,
42    },
43
44    /// Manifest and configuration errors
45    #[error("Invalid manifest file: {reason}")]
46    InvalidManifest { reason: String },
47
48    #[error("Configuration error: {reason}")]
49    ConfigError { reason: String },
50
51    /// Template errors
52    #[error("Template '{template}' not found. Available templates: {available}")]
53    TemplateNotFound { template: String, available: String },
54
55    #[error("Template error: {reason}")]
56    TemplateError { reason: String },
57
58    /// I/O and filesystem errors
59    #[error("File operation failed: {operation} on {path}")]
60    FileOperationFailed {
61        operation: String,
62        path: String,
63        #[source]
64        source: std::io::Error,
65    },
66
67    #[error("Permission denied: {operation} on {path}")]
68    PermissionDenied { operation: String, path: String },
69
70    /// Validation errors
71    #[error("Invalid input: {field} = '{value}'. {suggestion}")]
72    InvalidInput {
73        field: String,
74        value: String,
75        suggestion: String,
76    },
77
78    #[error("Validation failed: {reason}")]
79    ValidationError { reason: String },
80
81    /// Server and protocol errors
82    #[error("Server error: {message}")]
83    ServerError { message: String },
84
85    #[error("Protocol error: {reason}")]
86    ProtocolError { reason: String },
87
88    #[error("Unexpected response from server: {response}")]
89    UnexpectedResponse { response: String },
90
91    /// Event and monitoring errors
92    #[error("Event stream error: {reason}")]
93    EventStreamError { reason: String },
94
95    #[error("Event filter error: {filter} is invalid")]
96    EventFilterError { filter: String },
97
98    /// Generic wrapper for other errors
99    #[error("Internal error: {0}")]
100    Internal(#[from] anyhow::Error),
101
102    #[error("Serialization error: {0}")]
103    Serialization(#[from] serde_json::Error),
104
105    #[error("I/O error: {0}")]
106    Io(#[from] std::io::Error),
107
108    /// Network and protocol specific errors
109    #[error("Network error during {operation}: {source}")]
110    NetworkError {
111        operation: String,
112        source: Box<dyn std::error::Error + Send + Sync>,
113    },
114
115    #[error("Invalid response: {message}")]
116    InvalidResponse {
117        message: String,
118        source: Option<Box<dyn std::error::Error + Send + Sync>>,
119    },
120
121    #[error("I/O operation failed: {operation}")]
122    IoError {
123        operation: String,
124        #[source]
125        source: std::io::Error,
126    },
127
128    #[error("Parse error: {message}")]
129    ParseError { message: String },
130
131    #[error("Not implemented: {feature}")]
132    NotImplemented { feature: String, message: String },
133}
134
135impl CliError {
136    /// Create a connection failed error
137    pub fn connection_failed(address: SocketAddr, source: impl Into<anyhow::Error>) -> Self {
138        Self::ConnectionFailed {
139            address,
140            source: source.into(),
141        }
142    }
143
144    /// Create an actor not found error
145    pub fn actor_not_found(actor_id: impl Into<String>) -> Self {
146        Self::ActorNotFound {
147            actor_id: actor_id.into(),
148        }
149    }
150
151    /// Create a build failed error
152    pub fn build_failed(output: impl Into<String>) -> Self {
153        Self::BuildFailed {
154            output: output.into(),
155        }
156    }
157
158    /// Create an invalid manifest error
159    pub fn invalid_manifest(reason: impl Into<String>) -> Self {
160        Self::InvalidManifest {
161            reason: reason.into(),
162        }
163    }
164
165    /// Create a template not found error
166    pub fn template_not_found(template: impl Into<String>, available: Vec<String>) -> Self {
167        Self::TemplateNotFound {
168            template: template.into(),
169            available: available.join(", "),
170        }
171    }
172
173    /// Create a file operation failed error
174    pub fn file_operation_failed(
175        operation: impl Into<String>,
176        path: impl Into<String>,
177        source: std::io::Error,
178    ) -> Self {
179        Self::FileOperationFailed {
180            operation: operation.into(),
181            path: path.into(),
182            source,
183        }
184    }
185
186    /// Create an invalid input error with helpful suggestions
187    pub fn invalid_input(
188        field: impl Into<String>,
189        value: impl Into<String>,
190        suggestion: impl Into<String>,
191    ) -> Self {
192        Self::InvalidInput {
193            field: field.into(),
194            value: value.into(),
195            suggestion: suggestion.into(),
196        }
197    }
198
199    /// Create an invalid actor ID error
200    pub fn invalid_actor_id(actor_id: impl Into<String>) -> Self {
201        let actor_id = actor_id.into();
202        Self::InvalidInput {
203            field: "actor_id".to_string(),
204            value: actor_id,
205            suggestion:
206                "Actor ID must be a valid UUID (e.g., 123e4567-e89b-12d3-a456-426614174000)"
207                    .to_string(),
208        }
209    }
210
211    pub fn operation_timeout(_operation: impl Into<String>, timeout: u64) -> Self {
212        Self::ConnectionTimeout { timeout }
213    }
214
215    /// Create a not implemented error
216    pub fn not_implemented(feature: impl Into<String>, message: impl Into<String>) -> Self {
217        Self::NotImplemented {
218            feature: feature.into(),
219            message: message.into(),
220        }
221    }
222
223    /// Get a user-friendly error message with potential solutions
224    pub fn user_message(&self) -> String {
225        match self {
226            Self::ConnectionFailed { address, .. } => {
227                format!(
228                    "Could not connect to Theater server at {}.\n\n\
229                    Possible solutions:\n\
230                    • Start a Theater server with: theater server\n\
231                    • Check if the server address is correct\n\
232                    • Verify the server is running and accessible",
233                    address
234                )
235            }
236            Self::ActorNotFound { actor_id } => {
237                format!(
238                    "Actor '{}' was not found.\n\n\
239                    Possible solutions:\n\
240                    • Check the actor ID is correct\n\
241                    • List running actors with: theater list\n\
242                    • Start the actor with: theater start <manifest>",
243                    actor_id
244                )
245            }
246            Self::BuildFailed { output } => {
247                format!(
248                    "Build failed.\n\n\
249                    Build output:\n{}\n\n\
250                    Possible solutions:\n\
251                    • Check for compilation errors in your Rust code\n\
252                    • Ensure all dependencies are available\n\
253                    • Try a clean build with: theater build --clean",
254                    output
255                )
256            }
257            Self::MissingTool {
258                tool,
259                install_command,
260            } => {
261                format!(
262                    "Required tool '{}' is not installed.\n\n\
263                    Install it with:\n  {}",
264                    tool, install_command
265                )
266            }
267            Self::TemplateNotFound {
268                template,
269                available,
270            } => {
271                format!(
272                    "Template '{}' was not found.\n\n\
273                    Available templates: {}\n\n\
274                    Use one of the available templates or check your template configuration.",
275                    template, available
276                )
277            }
278            Self::InvalidInput {
279                field,
280                value,
281                suggestion,
282            } => {
283                format!("Invalid {}: '{}'\n\n{}", field, value, suggestion)
284            }
285            Self::NotImplemented { feature, message } => {
286                format!(
287                    "Feature '{}' is not implemented.\n\n\
288                    {}\n\n\
289                    ",
290                    feature, message
291                )
292            }
293            _ => self.to_string(),
294        }
295    }
296
297    /// Check if this error suggests the user should retry
298    pub fn is_retryable(&self) -> bool {
299        matches!(
300            self,
301            Self::ConnectionFailed { .. }
302                | Self::ConnectionLost
303                | Self::ConnectionTimeout { .. }
304                | Self::ServerError { .. }
305        )
306    }
307
308    /// Get the error category for metrics/logging
309    pub fn category(&self) -> &'static str {
310        match self {
311            Self::ConnectionFailed { .. }
312            | Self::ConnectionLost
313            | Self::ConnectionTimeout { .. } => "connection",
314            Self::ActorNotFound { .. }
315            | Self::ActorStartFailed { .. }
316            | Self::ActorNotRunning { .. } => "actor",
317            Self::InvalidProjectDirectory { .. }
318            | Self::BuildFailed { .. }
319            | Self::MissingTool { .. } => "build",
320            Self::InvalidManifest { .. } | Self::ConfigError { .. } => "config",
321            Self::TemplateNotFound { .. } | Self::TemplateError { .. } => "template",
322            Self::FileOperationFailed { .. } | Self::PermissionDenied { .. } => "filesystem",
323            Self::InvalidInput { .. } | Self::ValidationError { .. } => "validation",
324            Self::ServerError { .. }
325            | Self::ProtocolError { .. }
326            | Self::UnexpectedResponse { .. } => "server",
327            Self::EventStreamError { .. } | Self::EventFilterError { .. } => "events",
328            Self::Internal(_) | Self::Serialization(_) | Self::Io(_) | Self::ParseError { .. } => {
329                "internal"
330            }
331            Self::NetworkError { .. } => "network",
332            Self::InvalidResponse { .. } => "response",
333            Self::IoError { .. } => "io",
334            Self::NotImplemented { .. } => "not_implemented",
335        }
336    }
337}
338
339/// Result type alias for CLI operations
340pub type CliResult<T> = Result<T, CliError>;
341
342/// Extension trait for converting common errors to CliError
343pub trait IntoCliError<T> {
344    fn into_cli_error(self) -> CliResult<T>;
345    fn with_cli_context(self, context: impl FnOnce() -> CliError) -> CliResult<T>;
346}
347
348impl<T, E> IntoCliError<T> for Result<T, E>
349where
350    E: Into<anyhow::Error>,
351{
352    fn into_cli_error(self) -> CliResult<T> {
353        self.map_err(|e| CliError::Internal(e.into()))
354    }
355
356    fn with_cli_context(self, context: impl FnOnce() -> CliError) -> CliResult<T> {
357        self.map_err(|_| context())
358    }
359}
360
361#[cfg(test)]
362mod tests {
363    use super::*;
364
365    #[test]
366    fn test_error_categories() {
367        assert_eq!(CliError::actor_not_found("test").category(), "actor");
368        assert_eq!(CliError::build_failed("failed").category(), "build");
369        assert_eq!(
370            CliError::template_not_found("basic", vec!["http".to_string()]).category(),
371            "template"
372        );
373    }
374
375    #[test]
376    fn test_retryable_errors() {
377        assert!(CliError::ConnectionLost.is_retryable());
378        assert!(!CliError::actor_not_found("test").is_retryable());
379    }
380
381    #[test]
382    fn test_user_messages() {
383        let error = CliError::actor_not_found("test-actor");
384        let message = error.user_message();
385        assert!(message.contains("test-actor"));
386        assert!(message.contains("theater list"));
387    }
388}