1use std::fmt;
2
3use crate::types::component::Component;
4use crate::types::embed::Embed;
5
6#[derive(Default)]
16pub struct Response {
17 kind: ResponseKind,
18}
19
20#[derive(Default)]
21enum ResponseKind {
22 #[default]
24 Empty,
25 Message(Message),
27 Acknowledge,
29 File(FileResponse),
31}
32
33#[derive(Default)]
34struct Message {
35 content: String,
36 embeds: Vec<Embed>,
37 components: Vec<Component>,
38 ephemeral: bool,
39}
40
41pub struct FileResponse {
43 pub file: FileSource,
45 pub filename: Option<String>,
47 pub caption: Option<String>,
49}
50
51pub enum FileSource {
56 File(async_fs::File),
58 Bytes(Vec<u8>),
60 Path(std::path::PathBuf),
62}
63
64impl FileSource {
65 pub async fn read(self) -> std::io::Result<Vec<u8>> {
67 use futures_lite::io::AsyncReadExt;
68
69 match self {
70 Self::Bytes(bytes) => Ok(bytes),
71 Self::Path(path) => async_fs::read(path).await,
72 Self::File(mut file) => {
73 let mut bytes = Vec::new();
74 file.read_to_end(&mut bytes).await?;
75 Ok(bytes)
76 }
77 }
78 }
79
80 fn implied_filename(&self) -> Option<String> {
82 match self {
83 Self::Path(path) => Some(path.file_name()?.to_string_lossy().into_owned()),
84 _ => None,
85 }
86 }
87}
88
89impl fmt::Debug for FileSource {
90 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91 match self {
92 Self::File(_) => f.write_str("FileSource::File(..)"),
93 Self::Bytes(bytes) => write!(f, "FileSource::Bytes({} bytes)", bytes.len()),
94 Self::Path(path) => write!(f, "FileSource::Path({})", path.display()),
95 }
96 }
97}
98
99impl Response {
100 pub fn empty() -> Self {
102 Self {
103 kind: ResponseKind::Empty,
104 }
105 }
106
107 pub fn text(content: impl Into<String>) -> Self {
109 Self {
110 kind: ResponseKind::Message(Message {
111 content: content.into(),
112 ..Message::default()
113 }),
114 }
115 }
116
117 pub fn acknowledge() -> Self {
119 Self {
120 kind: ResponseKind::Acknowledge,
121 }
122 }
123
124 pub fn embed(embed: Embed) -> Self {
126 Self::empty().with_embed(embed)
127 }
128
129 pub fn file(file: async_fs::File) -> Self {
131 Self::from_source(FileSource::File(file))
132 }
133
134 pub fn bytes(bytes: impl Into<Vec<u8>>) -> Self {
136 Self::from_source(FileSource::Bytes(bytes.into()))
137 }
138
139 pub fn path(path: impl Into<std::path::PathBuf>) -> Self {
143 Self::from_source(FileSource::Path(path.into()))
144 }
145
146 fn from_source(file: FileSource) -> Self {
147 Self {
148 kind: ResponseKind::File(FileResponse {
149 filename: file.implied_filename(),
150 file,
151 caption: None,
152 }),
153 }
154 }
155
156 fn message_mut(&mut self) -> Option<&mut Message> {
161 if matches!(self.kind, ResponseKind::Empty) {
162 self.kind = ResponseKind::Message(Message::default());
163 }
164 match &mut self.kind {
165 ResponseKind::Message(message) => Some(message),
166 _ => None,
167 }
168 }
169
170 pub fn with_embed(mut self, embed: Embed) -> Self {
172 if let Some(message) = self.message_mut() {
173 message.embeds.push(embed);
174 }
175 self
176 }
177
178 pub fn with_components(mut self, components: Vec<Component>) -> Self {
180 if let Some(message) = self.message_mut() {
181 message.components = components;
182 }
183 self
184 }
185
186 pub fn ephemeral(mut self) -> Self {
190 if let Some(message) = self.message_mut() {
191 message.ephemeral = true;
192 }
193 self
194 }
195
196 pub fn with_filename(mut self, name: impl Into<String>) -> Self {
198 if let ResponseKind::File(file) = &mut self.kind {
199 file.filename = Some(name.into());
200 }
201 self
202 }
203
204 pub fn with_caption(mut self, caption: impl Into<String>) -> Self {
206 if let ResponseKind::File(file) = &mut self.kind {
207 file.caption = Some(caption.into());
208 }
209 self
210 }
211
212 pub fn is_empty(&self) -> bool {
214 match &self.kind {
215 ResponseKind::Empty => true,
216 ResponseKind::Message(message) => {
217 message.content.is_empty()
218 && message.embeds.is_empty()
219 && message.components.is_empty()
220 }
221 _ => false,
222 }
223 }
224
225 pub fn is_acknowledge(&self) -> bool {
227 matches!(self.kind, ResponseKind::Acknowledge)
228 }
229
230 pub fn content(&self) -> Option<&str> {
232 match &self.kind {
233 ResponseKind::Message(message) => Some(&message.content),
234 _ => None,
235 }
236 }
237
238 pub fn embeds(&self) -> &[Embed] {
240 match &self.kind {
241 ResponseKind::Message(message) => &message.embeds,
242 _ => &[],
243 }
244 }
245
246 pub fn components(&self) -> &[Component] {
248 match &self.kind {
249 ResponseKind::Message(message) => &message.components,
250 _ => &[],
251 }
252 }
253
254 pub fn is_ephemeral(&self) -> bool {
256 match &self.kind {
257 ResponseKind::Message(message) => message.ephemeral,
258 _ => false,
259 }
260 }
261
262 pub fn is_file(&self) -> bool {
264 matches!(self.kind, ResponseKind::File(_))
265 }
266
267 pub fn take_file(&mut self) -> Option<FileResponse> {
271 match std::mem::take(&mut self.kind) {
272 ResponseKind::File(file) => Some(file),
273 other => {
274 self.kind = other;
275 None
276 }
277 }
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284 use futures_lite::future::block_on;
285
286 #[test]
287 fn text_carries_content() {
288 let response = Response::text("hi");
289 assert_eq!(response.content(), Some("hi"));
290 assert!(!response.is_empty());
291 assert!(!response.is_file());
292 }
293
294 #[test]
295 fn empty_response_is_empty() {
296 assert!(Response::empty().is_empty());
297 assert!(Response::text("").is_empty());
298 assert!(!Response::acknowledge().is_empty());
299 }
300
301 #[test]
302 fn builders_promote_empty_instead_of_dropping_values() {
303 let response = Response::empty()
304 .with_embed(Embed::new().title("t"))
305 .ephemeral();
306 assert_eq!(response.embeds().len(), 1);
307 assert!(response.is_ephemeral());
308 assert!(!response.is_empty());
309 }
310
311 #[test]
312 fn embed_constructor_matches_builder() {
313 let response = Response::embed(Embed::new().title("t"));
314 assert_eq!(response.embeds().len(), 1);
315 assert_eq!(response.embeds()[0].title.as_deref(), Some("t"));
316 }
317
318 #[test]
319 fn message_builders_leave_file_responses_alone() {
320 let response = Response::bytes(b"data".to_vec())
321 .with_embed(Embed::new())
322 .ephemeral();
323 assert!(response.is_file());
324 assert!(response.embeds().is_empty());
325 assert!(!response.is_ephemeral());
326 }
327
328 #[test]
329 fn path_responses_default_their_filename() {
330 let response = Response::path("/tmp/report.pdf");
331 let mut response = response;
332 let file = response.take_file().unwrap();
333 assert_eq!(file.filename.as_deref(), Some("report.pdf"));
334 }
335
336 #[test]
337 fn take_file_yields_the_payload_once() {
338 let mut response = Response::bytes(b"hello".to_vec()).with_caption("cap");
339
340 let file = response.take_file().expect("first take yields the file");
341 assert_eq!(file.caption.as_deref(), Some("cap"));
342 assert_eq!(block_on(file.file.read()).unwrap(), b"hello");
343
344 assert!(response.take_file().is_none());
345 assert!(response.is_empty());
346 }
347
348 #[test]
349 fn take_file_preserves_non_file_responses() {
350 let mut response = Response::text("hi");
351 assert!(response.take_file().is_none());
352 assert_eq!(response.content(), Some("hi"));
353 }
354}