theater_cli/
error.rs

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