theater_cli/commands/
message.rs1use 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 #[arg(required = true)]
17 pub actor_id: String,
18
19 #[arg(required_unless_present = "file")]
21 pub message: Option<String>,
22
23 #[arg(short, long, conflicts_with = "message")]
25 pub file: Option<PathBuf>,
26
27 #[arg(short, long, default_value = "127.0.0.1:9000")]
29 pub address: SocketAddr,
30
31 #[arg(short, long, default_value = "false")]
33 pub request: bool,
34}
35
36pub async fn execute_async(args: &MessageArgs, ctx: &CommandContext) -> CliResult<()> {
38 debug!("Sending message to actor: {}", args.actor_id);
39
40 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 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 let client = ctx.create_client();
71 client
72 .connect()
73 .await
74 .map_err(|e| CliError::connection_failed(args.address, e))?;
75
76 let message_bytes = message_content.as_bytes().to_vec();
78
79 if args.request {
80 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 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 let format = if ctx.json { Some("json") } else { None };
97 ctx.output.output(&message_response, format)?;
98 } else {
99 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 let message_sent = MessageSent {
109 actor_id: actor_id.to_string(),
110 message: message_content,
111 success: true,
112 };
113
114 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}