theater_cli/commands/
message.rs

1use clap::Parser;
2use std::fs;
3use std::net::SocketAddr;
4use std::path::PathBuf;
5use std::str::FromStr;
6use tracing::debug;
7
8use crate::error::{CliError, CliResult};
9use crate::output::formatters::{MessageResponse, MessageSent};
10use crate::CommandContext;
11use theater::id::TheaterId;
12
13#[derive(Debug, Parser)]
14pub struct MessageArgs {
15    /// ID of the actor to send a message to
16    #[arg(required = true)]
17    pub actor_id: String,
18
19    /// Message to send (as string)
20    #[arg(required_unless_present = "file")]
21    pub message: Option<String>,
22
23    /// File containing message to send
24    #[arg(short, long, conflicts_with = "message")]
25    pub file: Option<PathBuf>,
26
27    /// Address of the theater server
28    #[arg(short, long, default_value = "127.0.0.1:9000")]
29    pub address: SocketAddr,
30
31    /// Send as a request (awaits response) instead of a one-way message
32    #[arg(short, long, default_value = "false")]
33    pub request: bool,
34}
35
36/// Execute the message command asynchronously with modern patterns
37pub async fn execute_async(args: &MessageArgs, ctx: &CommandContext) -> CliResult<()> {
38    debug!("Sending message to actor: {}", args.actor_id);
39
40    // Get message content either from direct argument or file
41    let message_content = if let Some(message) = &args.message {
42        message.clone()
43    } else if let Some(file_path) = &args.file {
44        debug!("Reading message from file: {:?}", file_path);
45        fs::read_to_string(file_path).map_err(|e| CliError::FileOperationFailed {
46            path: file_path.display().to_string(),
47            operation: "read".to_string(),
48            source: e,
49        })?
50    } else {
51        return Err(CliError::InvalidInput {
52            field: "message".to_string(),
53            value: "none".to_string(),
54            suggestion: "Either provide a message directly or specify a file with --file"
55                .to_string(),
56        });
57    };
58
59    debug!("Message: {}", message_content);
60    debug!("Connecting to server at: {}", args.address);
61
62    // Parse the actor ID
63    let actor_id = TheaterId::from_str(&args.actor_id).map_err(|_| CliError::InvalidInput {
64        field: "actor_id".to_string(),
65        value: args.actor_id.clone(),
66        suggestion: "Provide a valid actor ID in the correct format".to_string(),
67    })?;
68
69    // Create client and connect
70    let client = ctx.create_client();
71    client
72        .connect()
73        .await
74        .map_err(|e| CliError::connection_failed(args.address, e))?;
75
76    // Convert message to bytes
77    let message_bytes = message_content.as_bytes().to_vec();
78
79    if args.request {
80        // Send as a request and wait for response
81        let response: Vec<u8> = client
82            .request_message(&actor_id.to_string(), message_bytes)
83            .await
84            .map_err(|e| CliError::ServerError {
85                message: format!("Failed to send request to actor: {}", e),
86            })?;
87
88        // Create formatted output for response
89        let message_response = MessageResponse {
90            actor_id: actor_id.to_string(),
91            request: message_content,
92            response: String::from_utf8_lossy(&response).to_string(),
93        };
94
95        // Output using the configured format
96        let format = if ctx.json { Some("json") } else { None };
97        ctx.output.output(&message_response, format)?;
98    } else {
99        // Send as a one-way message
100        client
101            .send_message(&actor_id.to_string(), message_bytes)
102            .await
103            .map_err(|e| CliError::ServerError {
104                message: format!("Failed to send message to actor: {}", e),
105            })?;
106
107        // Create formatted output for message sent
108        let message_sent = MessageSent {
109            actor_id: actor_id.to_string(),
110            message: message_content,
111            success: true,
112        };
113
114        // Output using the configured format
115        let format = if ctx.json { Some("json") } else { None };
116        ctx.output.output(&message_sent, format)?;
117    }
118
119    Ok(())
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use crate::config::Config;
126    use crate::output::OutputManager;
127
128    #[tokio::test]
129    async fn test_message_command_invalid_actor_id() {
130        let args = MessageArgs {
131            actor_id: "invalid-id".to_string(),
132            message: Some("test message".to_string()),
133            file: None,
134            address: "127.0.0.1:9000".parse().unwrap(),
135            request: false,
136        };
137        let config = Config::default();
138        let output = OutputManager::new(config.output.clone());
139
140        let ctx = CommandContext {
141            config,
142            output,
143            verbose: false,
144            json: false,
145        };
146
147        let result = execute_async(&args, &ctx).await;
148        assert!(result.is_err());
149        if let Err(CliError::InvalidInput { field, .. }) = result {
150            assert_eq!(field, "actor_id");
151        } else {
152            panic!("Expected InvalidInput error");
153        }
154    }
155
156    #[tokio::test]
157    async fn test_message_command_no_message_or_file() {
158        let args = MessageArgs {
159            actor_id: "test-id".to_string(),
160            message: None,
161            file: None,
162            address: "127.0.0.1:9000".parse().unwrap(),
163            request: false,
164        };
165        let config = Config::default();
166        let output = OutputManager::new(config.output.clone());
167
168        let ctx = CommandContext {
169            config,
170            output,
171            verbose: false,
172            json: false,
173        };
174
175        let result = execute_async(&args, &ctx).await;
176        assert!(result.is_err());
177        if let Err(CliError::InvalidInput { field, .. }) = result {
178            assert_eq!(field, "message");
179        } else {
180            panic!("Expected InvalidInput error");
181        }
182    }
183}