azisaba_graph/apis/
stream_api.rs1use futures_core::Stream;
10use futures_util::StreamExt;
11use reqwest;
12use serde::{Deserialize, Serialize};
13
14use super::{configuration, Error};
15use crate::{apis::ResponseContent, models};
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19#[serde(untagged)]
20pub enum StreamEventsError {
21 Status401(),
22 Status403(),
23 UnknownValue(serde_json::Value),
24}
25
26pub async fn stream_events(
31 configuration: &configuration::Configuration,
32) -> Result<
33 impl Stream<Item = Result<models::StreamEvent, Error<StreamEventsError>>>,
34 Error<StreamEventsError>,
35> {
36 let uri = format!("{}/stream", configuration.base_path);
37 let mut request = configuration
38 .client
39 .request(reqwest::Method::GET, uri)
40 .header(reqwest::header::ACCEPT, "text/event-stream");
41
42 if let Some(ref user_agent) = configuration.user_agent {
43 request = request.header(reqwest::header::USER_AGENT, user_agent.clone());
44 }
45 if let Some(ref token) = configuration.bearer_access_token {
46 request = request.bearer_auth(token);
47 }
48
49 let response = configuration.client.execute(request.build()?).await?;
50 let status = response.status();
51
52 if status.is_client_error() || status.is_server_error() {
53 let content = response.text().await?;
54 let entity = serde_json::from_str(&content).ok();
55 return Err(Error::ResponseError(ResponseContent {
56 status,
57 content,
58 entity,
59 }));
60 }
61
62 let content_type = response
63 .headers()
64 .get(reqwest::header::CONTENT_TYPE)
65 .and_then(|value| value.to_str().ok())
66 .unwrap_or_default();
67 if !content_type
68 .to_ascii_lowercase()
69 .starts_with("text/event-stream")
70 {
71 return Err(Error::Io(std::io::Error::new(
72 std::io::ErrorKind::InvalidData,
73 format!("unexpected stream Content-Type: {content_type}"),
74 )));
75 }
76
77 let mut chunks = response.bytes_stream();
78 Ok(async_stream::try_stream! {
79 let mut parser = EventStreamParser::default();
80 while let Some(chunk) = chunks.next().await {
81 let chunk = chunk?;
82 for payload in parser.push(&chunk, false)? {
83 yield serde_json::from_str::<models::StreamEvent>(&payload)?;
84 }
85 }
86 for payload in parser.push(&[], true)? {
87 yield serde_json::from_str::<models::StreamEvent>(&payload)?;
88 }
89 })
90}
91
92#[derive(Default)]
93struct EventStreamParser {
94 buffer: Vec<u8>,
95 data: Vec<String>,
96}
97
98impl EventStreamParser {
99 fn push(&mut self, chunk: &[u8], end_of_stream: bool) -> std::io::Result<Vec<String>> {
100 self.buffer.extend_from_slice(chunk);
101 let mut payloads = Vec::new();
102
103 while let Some((line, consumed)) = next_line(&self.buffer, end_of_stream) {
104 let line = String::from_utf8(line.to_vec())
105 .map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error))?;
106 self.buffer.drain(..consumed);
107 if let Some(payload) = self.consume_line(&line) {
108 payloads.push(payload);
109 }
110 }
111
112 if end_of_stream {
113 if let Some(payload) = self.dispatch() {
114 payloads.push(payload);
115 }
116 }
117 Ok(payloads)
118 }
119
120 fn consume_line(&mut self, line: &str) -> Option<String> {
121 if line.is_empty() {
122 return self.dispatch();
123 }
124 if line.starts_with(':') {
125 return None;
126 }
127
128 let (field, value) = match line.split_once(':') {
129 Some((field, value)) => (field, value.strip_prefix(' ').unwrap_or(value)),
130 None => (line, ""),
131 };
132 if field == "data" {
133 self.data.push(value.to_owned());
134 }
135 None
136 }
137
138 fn dispatch(&mut self) -> Option<String> {
139 if self.data.is_empty() {
140 return None;
141 }
142 Some(std::mem::take(&mut self.data).join("\n"))
143 }
144}
145
146fn next_line(buffer: &[u8], end_of_stream: bool) -> Option<(&[u8], usize)> {
147 for (index, byte) in buffer.iter().enumerate() {
148 match byte {
149 b'\n' => return Some((&buffer[..index], index + 1)),
150 b'\r' => {
151 if index + 1 == buffer.len() && !end_of_stream {
152 return None;
153 }
154 let consumed = if buffer.get(index + 1) == Some(&b'\n') {
155 index + 2
156 } else {
157 index + 1
158 };
159 return Some((&buffer[..index], consumed));
160 }
161 _ => {}
162 }
163 }
164
165 if end_of_stream && !buffer.is_empty() {
166 Some((buffer, buffer.len()))
167 } else {
168 None
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::EventStreamParser;
175 use crate::models::StreamEvent;
176
177 const PLAYER: &str = r#"{
178 "id":"00000000-0000-0000-0000-000000000001",
179 "discordId":null,
180 "username":"player",
181 "status":"offline",
182 "currentServer":null,
183 "bio":null
184 }"#;
185
186 #[test]
187 fn parses_chunked_multiline_events_and_ignores_comments() {
188 let mut parser = EventStreamParser::default();
189 assert!(parser
190 .push(b": keep-alive\r\nda", false)
191 .unwrap()
192 .is_empty());
193 assert_eq!(
194 parser
195 .push(b"ta: {\"type\":\r\ndata: \"friend-added\"}\r\n\r\n", false)
196 .unwrap(),
197 vec!["{\"type\":\n\"friend-added\"}"]
198 );
199 }
200
201 #[test]
202 fn dispatches_the_last_event_when_the_connection_closes() {
203 let mut parser = EventStreamParser::default();
204 assert_eq!(
205 parser
206 .push(b"data: {\"type\":\"friend-added\"}", true)
207 .unwrap(),
208 vec!["{\"type\":\"friend-added\"}"]
209 );
210 }
211
212 #[test]
213 fn dispatches_structurally_identical_events_by_type() {
214 let payload = format!(
215 r#"{{"type":"friend-removed","data":{{"player":{PLAYER},"friend":{PLAYER}}}}}"#
216 );
217
218 assert!(matches!(
219 serde_json::from_str::<StreamEvent>(&payload).unwrap(),
220 StreamEvent::FriendRemoved(_)
221 ));
222 }
223
224 #[test]
225 fn rejects_unknown_event_types() {
226 let error =
227 serde_json::from_str::<StreamEvent>(r#"{"type":"unknown","data":{}}"#).unwrap_err();
228 assert!(error.to_string().contains("unknown variant"));
229 }
230}