1use crate::error::Error;
2use serde::Serialize;
3use std::time::Duration;
4
5#[must_use]
24#[derive(Debug, Clone)]
25pub struct Event {
26 pub(crate) id: String,
27 pub(crate) event: String,
28 pub(crate) data: Option<String>,
29 pub(crate) retry: Option<Duration>,
30}
31
32fn validate_field(value: &str, field_name: &str) -> Result<(), Error> {
33 if value.contains('\n') || value.contains('\r') {
34 return Err(Error::bad_request(format!(
35 "SSE {field_name} must not contain newline characters"
36 )));
37 }
38 Ok(())
39}
40
41impl Event {
42 pub fn new(id: impl Into<String>, event: impl Into<String>) -> Result<Self, Error> {
54 let id = id.into();
55 let event = event.into();
56 validate_field(&id, "id")?;
57 validate_field(&event, "event")?;
58 Ok(Self {
59 id,
60 event,
61 data: None,
62 retry: None,
63 })
64 }
65
66 pub fn data(mut self, data: impl Into<String>) -> Self {
71 self.data = Some(data.into());
72 self
73 }
74
75 pub fn json<T: Serialize>(mut self, data: &T) -> Result<Self, Error> {
83 let json = serde_json::to_string(data)
84 .map_err(|e| Error::internal(format!("SSE JSON serialization failed: {e}")))?;
85 self.data = Some(json);
86 Ok(self)
87 }
88
89 pub fn html(self, html: impl Into<String>) -> Self {
94 self.data(html)
95 }
96
97 pub fn retry(mut self, duration: Duration) -> Self {
102 self.retry = Some(duration);
103 self
104 }
105
106 pub fn id(&self) -> &str {
108 &self.id
109 }
110
111 pub fn event_name(&self) -> &str {
113 &self.event
114 }
115
116 pub fn data_ref(&self) -> Option<&str> {
118 self.data.as_deref()
119 }
120}
121
122impl From<Event> for axum::response::sse::Event {
123 fn from(event: Event) -> Self {
124 let mut axum_event = axum::response::sse::Event::default();
125 axum_event = axum_event.id(event.id);
126 axum_event = axum_event.event(event.event);
127 if let Some(data) = event.data {
128 axum_event = axum_event.data(data);
129 }
130 if let Some(retry) = event.retry {
131 axum_event = axum_event.retry(retry);
132 }
133 axum_event
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn new_with_valid_id_and_event() {
143 let event = Event::new("evt_01", "message").unwrap();
144 assert_eq!(event.id, "evt_01");
145 assert_eq!(event.event, "message");
146 assert!(event.data.is_none());
147 assert!(event.retry.is_none());
148 }
149
150 #[test]
151 fn new_rejects_newline_in_id() {
152 let result = Event::new("evt\n01", "message");
153 assert!(result.is_err());
154 assert!(result.unwrap_err().message().contains("id"));
155 }
156
157 #[test]
158 fn new_rejects_carriage_return_in_event() {
159 let result = Event::new("evt_01", "msg\r");
160 assert!(result.is_err());
161 assert!(result.unwrap_err().message().contains("event"));
162 }
163
164 #[test]
165 fn data_sets_payload() {
166 let event = Event::new("id", "ev").unwrap().data("hello");
167 assert_eq!(event.data.as_deref(), Some("hello"));
168 }
169
170 #[test]
171 fn json_serializes_payload() {
172 #[derive(serde::Serialize)]
173 struct Msg {
174 text: String,
175 }
176 let event = Event::new("id", "ev")
177 .unwrap()
178 .json(&Msg { text: "hi".into() })
179 .unwrap();
180 assert_eq!(event.data.as_deref(), Some(r#"{"text":"hi"}"#));
181 }
182
183 #[test]
184 fn html_sets_payload() {
185 let event = Event::new("id", "ev").unwrap().html("<div>hi</div>");
186 assert_eq!(event.data.as_deref(), Some("<div>hi</div>"));
187 }
188
189 #[test]
190 fn retry_sets_duration() {
191 let event = Event::new("id", "ev")
192 .unwrap()
193 .retry(std::time::Duration::from_secs(5));
194 assert_eq!(event.retry, Some(std::time::Duration::from_secs(5)));
195 }
196
197 #[test]
198 fn from_converts_to_axum_event() {
199 let event = Event::new("id1", "message")
200 .unwrap()
201 .data("hello")
202 .retry(std::time::Duration::from_millis(3000));
203 let axum_event: axum::response::sse::Event = event.into();
204 let _ = axum_event;
205 }
206
207 #[test]
208 fn data_methods_replace_previous() {
209 let event = Event::new("id", "ev").unwrap().data("first").html("second");
210 assert_eq!(event.data.as_deref(), Some("second"));
211 }
212
213 #[test]
214 fn new_with_empty_id_and_event_succeeds() {
215 let event = Event::new("", "").unwrap();
216 assert_eq!(event.id, "");
217 assert_eq!(event.event, "");
218 }
219
220 #[test]
221 fn new_rejects_carriage_return_in_id() {
222 let result = Event::new("evt\r01", "message");
223 assert!(result.is_err());
224 assert!(result.unwrap_err().message().contains("id"));
225 }
226
227 #[test]
228 fn new_rejects_newline_in_event() {
229 let result = Event::new("evt_01", "msg\n");
230 assert!(result.is_err());
231 assert!(result.unwrap_err().message().contains("event"));
232 }
233
234 #[test]
235 fn getter_methods_return_expected_values() {
236 let event = Event::new("id1", "update").unwrap().data("payload");
237 assert_eq!(event.id(), "id1");
238 assert_eq!(event.event_name(), "update");
239 assert_eq!(event.data_ref(), Some("payload"));
240 }
241
242 #[test]
243 fn data_ref_returns_none_when_no_data() {
244 let event = Event::new("id1", "ping").unwrap();
245 assert!(event.data_ref().is_none());
246 }
247}