Skip to main content

botkit_core/
responder.rs

1use crate::response::Response;
2
3#[cfg(not(target_arch = "wasm32"))]
4pub trait IntoResponseBounds: Send {}
5#[cfg(not(target_arch = "wasm32"))]
6impl<T: Send + ?Sized> IntoResponseBounds for T {}
7
8#[cfg(target_arch = "wasm32")]
9pub trait IntoResponseBounds {}
10#[cfg(target_arch = "wasm32")]
11impl<T: ?Sized> IntoResponseBounds for T {}
12
13/// Trait for converting types into bot responses
14///
15/// Similar to skyzen's `Responder` trait, this allows handlers to
16/// return various types that get converted to responses.
17///
18/// # Example
19/// ```ignore
20/// // Return a string directly
21/// async fn ping() -> &'static str {
22///     "Pong!"
23/// }
24///
25/// // Return a formatted string
26/// async fn greet(user: User) -> String {
27///     format!("Hello, {}!", user.name)
28/// }
29///
30/// // Return Response for full control
31/// async fn buttons() -> Response {
32///     Response::text("Click a button:")
33///         .with_components(vec![...])
34/// }
35/// ```
36pub trait IntoResponse: IntoResponseBounds {
37    /// Convert this type into a bot response
38    fn into_response(self) -> Response;
39}
40
41// String types
42impl IntoResponse for String {
43    fn into_response(self) -> Response {
44        Response::text(self)
45    }
46}
47
48impl IntoResponse for &'static str {
49    fn into_response(self) -> Response {
50        Response::text(self)
51    }
52}
53
54impl IntoResponse for std::borrow::Cow<'static, str> {
55    fn into_response(self) -> Response {
56        Response::text(self)
57    }
58}
59
60// Response passes through unchanged
61impl IntoResponse for Response {
62    fn into_response(self) -> Response {
63        self
64    }
65}
66
67// Unit type returns empty response
68impl IntoResponse for () {
69    fn into_response(self) -> Response {
70        Response::empty()
71    }
72}
73
74// Option<T> - None returns empty response
75impl<T: IntoResponse> IntoResponse for Option<T> {
76    fn into_response(self) -> Response {
77        match self {
78            Some(value) => value.into_response(),
79            None => Response::empty(),
80        }
81    }
82}
83
84/// `Err` is rendered to the user as `Error: {e}`.
85///
86/// Return `Response` directly when a handler needs to control what a failure
87/// looks like, or keep the error type's `Display` user-facing.
88impl<T, E> IntoResponse for Result<T, E>
89where
90    T: IntoResponse,
91    E: std::fmt::Display + IntoResponseBounds,
92{
93    fn into_response(self) -> Response {
94        match self {
95            Ok(value) => value.into_response(),
96            Err(e) => Response::text(format!("Error: {e}")),
97        }
98    }
99}
100
101// File responses
102impl IntoResponse for async_fs::File {
103    fn into_response(self) -> Response {
104        Response::file(self)
105    }
106}
107
108impl IntoResponse for std::path::PathBuf {
109    fn into_response(self) -> Response {
110        Response::path(self)
111    }
112}
113
114/// A file paired with a caption.
115///
116/// Only file-shaped payloads get this impl: `with_caption` is meaningless for a
117/// text response, so `("text", "caption")` is a compile error rather than a
118/// silently dropped caption.
119macro_rules! impl_captioned_file {
120    ($ty:ty) => {
121        impl<C: Into<String> + IntoResponseBounds> IntoResponse for ($ty, C) {
122            fn into_response(self) -> Response {
123                let (file, caption) = self;
124                file.into_response().with_caption(caption)
125            }
126        }
127    };
128}
129
130impl_captioned_file!(async_fs::File);
131impl_captioned_file!(std::path::PathBuf);
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn strings_become_text_responses() {
139        assert_eq!("hi".into_response().content(), Some("hi"));
140        assert_eq!(String::from("hi").into_response().content(), Some("hi"));
141        assert_eq!(
142            std::borrow::Cow::Borrowed("hi").into_response().content(),
143            Some("hi")
144        );
145    }
146
147    #[test]
148    fn unit_and_none_are_empty() {
149        assert!(().into_response().is_empty());
150        assert!(Option::<String>::None.into_response().is_empty());
151        assert_eq!(Some("hi").into_response().content(), Some("hi"));
152    }
153
154    #[test]
155    fn errors_render_their_display() {
156        let result: Result<&str, std::fmt::Error> = Err(std::fmt::Error);
157        assert_eq!(
158            result.into_response().content(),
159            Some("Error: an error occurred when formatting an argument")
160        );
161    }
162
163    #[test]
164    fn ok_passes_the_inner_value_through() {
165        let result: Result<&str, std::fmt::Error> = Ok("fine");
166        assert_eq!(result.into_response().content(), Some("fine"));
167    }
168
169    #[test]
170    fn captions_attach_to_file_responses() {
171        let mut response = (std::path::PathBuf::from("/tmp/a.txt"), "caption").into_response();
172        let file = response.take_file().unwrap();
173        assert_eq!(file.caption.as_deref(), Some("caption"));
174        assert_eq!(file.filename.as_deref(), Some("a.txt"));
175    }
176}