use std::fmt;
use crate::types::component::Component;
use crate::types::embed::Embed;
#[derive(Default)]
pub struct Response {
kind: ResponseKind,
}
#[derive(Default)]
enum ResponseKind {
#[default]
Empty,
Message(Message),
Acknowledge,
File(FileResponse),
}
#[derive(Default)]
struct Message {
content: String,
embeds: Vec<Embed>,
components: Vec<Component>,
ephemeral: bool,
}
pub struct FileResponse {
pub file: FileSource,
pub filename: Option<String>,
pub caption: Option<String>,
}
pub enum FileSource {
File(async_fs::File),
Bytes(Vec<u8>),
Path(std::path::PathBuf),
}
impl FileSource {
pub async fn read(self) -> std::io::Result<Vec<u8>> {
use futures_lite::io::AsyncReadExt;
match self {
Self::Bytes(bytes) => Ok(bytes),
Self::Path(path) => async_fs::read(path).await,
Self::File(mut file) => {
let mut bytes = Vec::new();
file.read_to_end(&mut bytes).await?;
Ok(bytes)
}
}
}
fn implied_filename(&self) -> Option<String> {
match self {
Self::Path(path) => Some(path.file_name()?.to_string_lossy().into_owned()),
_ => None,
}
}
}
impl fmt::Debug for FileSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::File(_) => f.write_str("FileSource::File(..)"),
Self::Bytes(bytes) => write!(f, "FileSource::Bytes({} bytes)", bytes.len()),
Self::Path(path) => write!(f, "FileSource::Path({})", path.display()),
}
}
}
impl Response {
pub fn empty() -> Self {
Self {
kind: ResponseKind::Empty,
}
}
pub fn text(content: impl Into<String>) -> Self {
Self {
kind: ResponseKind::Message(Message {
content: content.into(),
..Message::default()
}),
}
}
pub fn acknowledge() -> Self {
Self {
kind: ResponseKind::Acknowledge,
}
}
pub fn embed(embed: Embed) -> Self {
Self::empty().with_embed(embed)
}
pub fn file(file: async_fs::File) -> Self {
Self::from_source(FileSource::File(file))
}
pub fn bytes(bytes: impl Into<Vec<u8>>) -> Self {
Self::from_source(FileSource::Bytes(bytes.into()))
}
pub fn path(path: impl Into<std::path::PathBuf>) -> Self {
Self::from_source(FileSource::Path(path.into()))
}
fn from_source(file: FileSource) -> Self {
Self {
kind: ResponseKind::File(FileResponse {
filename: file.implied_filename(),
file,
caption: None,
}),
}
}
fn message_mut(&mut self) -> Option<&mut Message> {
if matches!(self.kind, ResponseKind::Empty) {
self.kind = ResponseKind::Message(Message::default());
}
match &mut self.kind {
ResponseKind::Message(message) => Some(message),
_ => None,
}
}
pub fn with_embed(mut self, embed: Embed) -> Self {
if let Some(message) = self.message_mut() {
message.embeds.push(embed);
}
self
}
pub fn with_components(mut self, components: Vec<Component>) -> Self {
if let Some(message) = self.message_mut() {
message.components = components;
}
self
}
pub fn ephemeral(mut self) -> Self {
if let Some(message) = self.message_mut() {
message.ephemeral = true;
}
self
}
pub fn with_filename(mut self, name: impl Into<String>) -> Self {
if let ResponseKind::File(file) = &mut self.kind {
file.filename = Some(name.into());
}
self
}
pub fn with_caption(mut self, caption: impl Into<String>) -> Self {
if let ResponseKind::File(file) = &mut self.kind {
file.caption = Some(caption.into());
}
self
}
pub fn is_empty(&self) -> bool {
match &self.kind {
ResponseKind::Empty => true,
ResponseKind::Message(message) => {
message.content.is_empty()
&& message.embeds.is_empty()
&& message.components.is_empty()
}
_ => false,
}
}
pub fn is_acknowledge(&self) -> bool {
matches!(self.kind, ResponseKind::Acknowledge)
}
pub fn content(&self) -> Option<&str> {
match &self.kind {
ResponseKind::Message(message) => Some(&message.content),
_ => None,
}
}
pub fn embeds(&self) -> &[Embed] {
match &self.kind {
ResponseKind::Message(message) => &message.embeds,
_ => &[],
}
}
pub fn components(&self) -> &[Component] {
match &self.kind {
ResponseKind::Message(message) => &message.components,
_ => &[],
}
}
pub fn is_ephemeral(&self) -> bool {
match &self.kind {
ResponseKind::Message(message) => message.ephemeral,
_ => false,
}
}
pub fn is_file(&self) -> bool {
matches!(self.kind, ResponseKind::File(_))
}
pub fn take_file(&mut self) -> Option<FileResponse> {
match std::mem::take(&mut self.kind) {
ResponseKind::File(file) => Some(file),
other => {
self.kind = other;
None
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use futures_lite::future::block_on;
#[test]
fn text_carries_content() {
let response = Response::text("hi");
assert_eq!(response.content(), Some("hi"));
assert!(!response.is_empty());
assert!(!response.is_file());
}
#[test]
fn empty_response_is_empty() {
assert!(Response::empty().is_empty());
assert!(Response::text("").is_empty());
assert!(!Response::acknowledge().is_empty());
}
#[test]
fn builders_promote_empty_instead_of_dropping_values() {
let response = Response::empty()
.with_embed(Embed::new().title("t"))
.ephemeral();
assert_eq!(response.embeds().len(), 1);
assert!(response.is_ephemeral());
assert!(!response.is_empty());
}
#[test]
fn embed_constructor_matches_builder() {
let response = Response::embed(Embed::new().title("t"));
assert_eq!(response.embeds().len(), 1);
assert_eq!(response.embeds()[0].title.as_deref(), Some("t"));
}
#[test]
fn message_builders_leave_file_responses_alone() {
let response = Response::bytes(b"data".to_vec())
.with_embed(Embed::new())
.ephemeral();
assert!(response.is_file());
assert!(response.embeds().is_empty());
assert!(!response.is_ephemeral());
}
#[test]
fn path_responses_default_their_filename() {
let response = Response::path("/tmp/report.pdf");
let mut response = response;
let file = response.take_file().unwrap();
assert_eq!(file.filename.as_deref(), Some("report.pdf"));
}
#[test]
fn take_file_yields_the_payload_once() {
let mut response = Response::bytes(b"hello".to_vec()).with_caption("cap");
let file = response.take_file().expect("first take yields the file");
assert_eq!(file.caption.as_deref(), Some("cap"));
assert_eq!(block_on(file.file.read()).unwrap(), b"hello");
assert!(response.take_file().is_none());
assert!(response.is_empty());
}
#[test]
fn take_file_preserves_non_file_responses() {
let mut response = Response::text("hi");
assert!(response.take_file().is_none());
assert_eq!(response.content(), Some("hi"));
}
}