Skip to main content

act_sdk/
response.rs

1use crate::context::RawStreamEvent;
2
3/// Trait for types that can be converted into stream events.
4pub trait IntoResponse {
5    fn into_stream_events(self, default_language: &str) -> Vec<RawStreamEvent>;
6}
7
8impl IntoResponse for String {
9    fn into_stream_events(self, _default_language: &str) -> Vec<RawStreamEvent> {
10        vec![RawStreamEvent::Content {
11            data: self.into_bytes(),
12            mime_type: Some(crate::constants::MIME_TEXT.to_string()),
13            metadata: vec![],
14        }]
15    }
16}
17
18impl IntoResponse for &str {
19    fn into_stream_events(self, default_language: &str) -> Vec<RawStreamEvent> {
20        self.to_string().into_stream_events(default_language)
21    }
22}
23
24impl IntoResponse for () {
25    fn into_stream_events(self, _default_language: &str) -> Vec<RawStreamEvent> {
26        vec![]
27    }
28}
29
30impl IntoResponse for Vec<u8> {
31    fn into_stream_events(self, _default_language: &str) -> Vec<RawStreamEvent> {
32        vec![RawStreamEvent::Content {
33            data: self,
34            mime_type: Some(crate::constants::MIME_OCTET_STREAM.to_string()),
35            metadata: vec![],
36        }]
37    }
38}
39
40/// Wrapper that serializes the inner value as JSON with `application/json` MIME type.
41pub struct Json<T>(pub T);
42
43impl<T: serde::Serialize> IntoResponse for Json<T> {
44    fn into_stream_events(self, _default_language: &str) -> Vec<RawStreamEvent> {
45        vec![RawStreamEvent::Content {
46            data: serde_json::to_vec(&self.0).unwrap_or_default(),
47            mime_type: Some(crate::constants::MIME_JSON.to_string()),
48            metadata: vec![],
49        }]
50    }
51}
52
53/// Wrapper for returning content with an explicit MIME type.
54///
55/// The first field is the MIME type string, the second is the raw data.
56pub struct Content(pub &'static str, pub Vec<u8>);
57
58impl IntoResponse for Content {
59    fn into_stream_events(self, _default_language: &str) -> Vec<RawStreamEvent> {
60        vec![RawStreamEvent::Content {
61            data: self.1,
62            mime_type: Some(self.0.to_string()),
63            metadata: vec![],
64        }]
65    }
66}
67
68/// Encode a serializable value as CBOR and wrap it as a stream event.
69///
70/// Used by the `#[act_tool]` macro for automatic CBOR serialization of
71/// structured return types.
72#[doc(hidden)]
73pub fn cbor_encode_response<T: serde::Serialize>(
74    val: &T,
75    _default_language: &str,
76) -> Vec<RawStreamEvent> {
77    let mut buf = Vec::new();
78    ciborium::into_writer(val, &mut buf).expect("CBOR serialization should not fail");
79    vec![RawStreamEvent::Content {
80        data: buf,
81        mime_type: Some(crate::constants::MIME_CBOR.to_string()),
82        metadata: vec![],
83    }]
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use serde_json::json;
90
91    fn extract_content(events: Vec<RawStreamEvent>) -> (Vec<u8>, Option<String>) {
92        match events.into_iter().next().unwrap() {
93            RawStreamEvent::Content {
94                data, mime_type, ..
95            } => (data, mime_type),
96            _ => panic!("expected Content event"),
97        }
98    }
99
100    #[test]
101    fn json_wrapper_produces_json_mime() {
102        let value = json!({"rows": [1, 2, 3]});
103        let events = Json(value.clone()).into_stream_events("en");
104        let (data, mime) = extract_content(events);
105        assert_eq!(mime.as_deref(), Some(crate::constants::MIME_JSON));
106        let parsed: serde_json::Value = serde_json::from_slice(&data).unwrap();
107        assert_eq!(parsed, value);
108    }
109
110    #[test]
111    fn content_wrapper_explicit_mime() {
112        let png_header = vec![0x89, 0x50, 0x4E, 0x47];
113        let events = Content("image/png", png_header.clone()).into_stream_events("en");
114        let (data, mime) = extract_content(events);
115        assert_eq!(mime.as_deref(), Some("image/png"));
116        assert_eq!(data, png_header);
117    }
118
119    #[test]
120    fn cbor_encode_produces_cbor_mime() {
121        let input = vec![1u32, 2, 3];
122        let events = cbor_encode_response(&input, "en");
123        let (data, mime) = extract_content(events);
124        assert_eq!(mime.as_deref(), Some(crate::constants::MIME_CBOR));
125        let decoded: Vec<u32> = ciborium::from_reader(&data[..]).unwrap();
126        assert_eq!(decoded, input);
127    }
128
129    #[test]
130    fn vec_u8_produces_octet_stream() {
131        let events = vec![1u8, 2, 3].into_stream_events("en");
132        let (_data, mime) = extract_content(events);
133        assert_eq!(mime.as_deref(), Some(crate::constants::MIME_OCTET_STREAM));
134    }
135
136    #[test]
137    fn string_still_produces_text_plain() {
138        let events = "hello".to_string().into_stream_events("en");
139        let (data, mime) = extract_content(events);
140        assert_eq!(mime.as_deref(), Some(crate::constants::MIME_TEXT));
141        assert_eq!(data, b"hello");
142    }
143}