Skip to main content

eventuary_core/
payload.rs

1use std::fmt;
2use std::str::FromStr;
3
4use bytes::Bytes;
5use serde::de::DeserializeOwned;
6use serde::{Deserialize, Serialize};
7
8use crate::error::{Error, Result};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11pub enum ContentType {
12    #[serde(rename = "application/json")]
13    Json,
14    #[serde(rename = "text/plain")]
15    PlainText,
16    #[serde(rename = "application/octet-stream")]
17    Binary,
18}
19
20impl fmt::Display for ContentType {
21    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22        match self {
23            ContentType::Json => write!(f, "application/json"),
24            ContentType::PlainText => write!(f, "text/plain"),
25            ContentType::Binary => write!(f, "application/octet-stream"),
26        }
27    }
28}
29
30impl FromStr for ContentType {
31    type Err = Error;
32
33    fn from_str(s: &str) -> Result<Self> {
34        match s {
35            "application/json" => Ok(ContentType::Json),
36            "text/plain" => Ok(ContentType::PlainText),
37            "application/octet-stream" => Ok(ContentType::Binary),
38            other => Err(Error::InvalidPayload(format!(
39                "unknown content type: {other}"
40            ))),
41        }
42    }
43}
44
45#[derive(Debug, Clone)]
46pub struct Payload {
47    data: Bytes,
48    content_type: ContentType,
49}
50
51impl Payload {
52    pub fn from_json<T: Serialize>(val: &T) -> Result<Self> {
53        let data = serde_json::to_vec(val).map_err(|e| Error::Serialization(e.to_string()))?;
54        Ok(Self {
55            data: Bytes::from(data),
56            content_type: ContentType::Json,
57        })
58    }
59
60    pub fn from_string(s: impl Into<String>) -> Self {
61        Self {
62            data: Bytes::from(s.into().into_bytes()),
63            content_type: ContentType::PlainText,
64        }
65    }
66
67    pub fn from_bytes(data: impl Into<Bytes>) -> Self {
68        Self {
69            data: data.into(),
70            content_type: ContentType::Binary,
71        }
72    }
73
74    pub fn from_raw(data: impl Into<Bytes>, content_type: ContentType) -> Self {
75        Self {
76            data: data.into(),
77            content_type,
78        }
79    }
80
81    pub fn to_json<T: DeserializeOwned>(&self) -> Result<T> {
82        if self.content_type != ContentType::Json {
83            return Err(Error::InvalidPayload("not JSON content".into()));
84        }
85        serde_json::from_slice(&self.data).map_err(|e| Error::Serialization(e.to_string()))
86    }
87
88    pub fn data(&self) -> &[u8] {
89        &self.data
90    }
91
92    pub fn content_type(&self) -> ContentType {
93        self.content_type
94    }
95
96    pub fn size(&self) -> usize {
97        self.data.len()
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn json_round_trip() {
107        let payload = Payload::from_json(&serde_json::json!({"k": "v"})).unwrap();
108        assert_eq!(payload.content_type(), ContentType::Json);
109        let value: serde_json::Value = payload.to_json().unwrap();
110        assert_eq!(value, serde_json::json!({"k": "v"}));
111    }
112
113    #[test]
114    fn plain_text_payload() {
115        let payload = Payload::from_string("hello");
116        assert_eq!(payload.content_type(), ContentType::PlainText);
117        assert_eq!(payload.data(), b"hello");
118        assert_eq!(payload.size(), 5);
119    }
120
121    #[test]
122    fn binary_payload_preserves_bytes() {
123        let bytes = vec![0xff, 0x00, 0x01, 0xfe];
124        let payload = Payload::from_bytes(bytes.clone());
125        assert_eq!(payload.content_type(), ContentType::Binary);
126        assert_eq!(payload.data(), bytes.as_slice());
127    }
128
129    #[test]
130    fn from_bytes_accepts_static_slice() {
131        let payload = Payload::from_bytes(Bytes::from_static(b"\x00\x01\x02"));
132        assert_eq!(payload.data(), &[0x00, 0x01, 0x02]);
133    }
134
135    #[test]
136    fn clone_does_not_copy_data() {
137        let payload = Payload::from_bytes(vec![1u8; 1024]);
138        let cloned = payload.clone();
139        assert_eq!(payload.data().as_ptr(), cloned.data().as_ptr());
140    }
141
142    #[test]
143    fn to_json_fails_on_non_json() {
144        let payload = Payload::from_string("hello");
145        let res: Result<serde_json::Value> = payload.to_json();
146        assert!(matches!(res, Err(Error::InvalidPayload(_))));
147    }
148
149    #[test]
150    fn content_type_round_trip() {
151        for ct in [
152            ContentType::Json,
153            ContentType::PlainText,
154            ContentType::Binary,
155        ] {
156            let s = ct.to_string();
157            let parsed: ContentType = s.parse().unwrap();
158            assert_eq!(parsed, ct);
159        }
160    }
161}