uptrakit_openapi_client/
batch_progress_stream.rs1use crate::sse::{self, RawSseEvent, SseError};
8use crate::{ClientError, Result, UptrakitClient};
9use rootcause::prelude::*;
10use serde::Deserialize;
11use uuid::Uuid;
12
13#[derive(Debug, Clone)]
15pub enum BatchProgressEvent {
16 Update(BatchUpdateEventData),
18 Progress(BatchProgressData),
20 BatchCompleted(BatchCompletedData),
22}
23
24#[derive(Debug, Clone, Deserialize)]
26pub struct BatchUpdateEventData {
27 pub event: String,
28 pub update_history_id: Uuid,
29 pub software_item_name: String,
30 pub host_name: String,
31 #[serde(default)]
32 pub error: Option<String>,
33}
34
35#[derive(Debug, Clone, Deserialize)]
37pub struct BatchProgressData {
38 pub completed: i64,
39 pub failed: i64,
40 pub pending: i64,
41 pub total: i32,
42}
43
44#[derive(Debug, Clone, Deserialize)]
46pub struct BatchCompletedData {
47 pub status: String,
48}
49
50#[derive(Debug, thiserror::Error)]
52pub enum StreamError {
53 #[error("SSE transport error: {0}")]
54 Sse(#[from] SseError),
55
56 #[error("failed to parse SSE event data: {0}")]
57 Parse(#[from] serde_json::Error),
58}
59
60impl UptrakitClient {
61 pub async fn stream_batch_progress(
68 &self,
69 id: &Uuid,
70 ) -> Result<
71 impl futures_util::Stream<Item = std::result::Result<BatchProgressEvent, StreamError>>,
72 > {
73 let url = format!(
74 "{}{}",
75 self.base_url,
76 crate::paths::update_batches::stream(id)
77 );
78
79 let mut req = self
80 .http
81 .get(&url)
82 .header("Accept", "text/event-stream")
83 .timeout(std::time::Duration::from_secs(86400));
84
85 if let Some(token) = &self.token {
86 req = req.bearer_auth(token);
87 }
88
89 let resp = req.send().await.context_to()?;
90
91 let status = resp.status();
92 if status == reqwest::StatusCode::UNAUTHORIZED {
93 bail!(ClientError::NotAuthenticated);
94 }
95 if status == reqwest::StatusCode::NOT_FOUND {
96 let text = resp.text().await.context_to()?;
97 let message = crate::extract_error_message(&text);
98 bail!(ClientError::NotFound(message));
99 }
100 if status.is_client_error() || status.is_server_error() {
101 let text = resp.text().await.context_to()?;
102 let message = crate::extract_error_message(&text);
103 bail!(ClientError::Api { status, message });
104 }
105
106 let raw_stream = sse::parse_sse_stream(resp);
107
108 let typed_stream = futures_util::StreamExt::filter_map(raw_stream, |result| async move {
109 match result {
110 Ok(event) => parse_typed_event(event),
111 Err(e) => Some(Err(StreamError::Sse(e))),
112 }
113 });
114
115 Ok(typed_stream)
116 }
117}
118
119fn parse_typed_event(
121 event: RawSseEvent,
122) -> Option<std::result::Result<BatchProgressEvent, StreamError>> {
123 match event.event_type.as_str() {
124 "update" => {
125 let parsed: std::result::Result<BatchUpdateEventData, _> =
126 serde_json::from_str(&event.data);
127 Some(parsed.map(BatchProgressEvent::Update).map_err(Into::into))
128 }
129 "progress" => {
130 let parsed: std::result::Result<BatchProgressData, _> =
131 serde_json::from_str(&event.data);
132 Some(parsed.map(BatchProgressEvent::Progress).map_err(Into::into))
133 }
134 "batch_completed" => {
135 let parsed: std::result::Result<BatchCompletedData, _> =
136 serde_json::from_str(&event.data);
137 Some(
138 parsed
139 .map(BatchProgressEvent::BatchCompleted)
140 .map_err(Into::into),
141 )
142 }
143 _ => None,
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn parse_update_event() {
153 let event = RawSseEvent {
154 event_type: "update".to_string(),
155 data: r#"{"event":"update_completed","update_history_id":"01234567-89ab-cdef-0123-456789abcdef","software_item_name":"nginx","host_name":"web-01"}"#.to_string(),
156 id: None,
157 };
158 let result = parse_typed_event(event).expect("should produce event");
159 let typed = result.expect("should parse");
160 assert!(
161 matches!(typed, BatchProgressEvent::Update(ref u) if u.software_item_name == "nginx")
162 );
163 }
164
165 #[test]
166 fn parse_progress_event() {
167 let event = RawSseEvent {
168 event_type: "progress".to_string(),
169 data: r#"{"event":"progress","completed":2,"failed":0,"pending":3,"total":5}"#
170 .to_string(),
171 id: None,
172 };
173 let result = parse_typed_event(event).expect("should produce event");
174 let typed = result.expect("should parse");
175 assert!(matches!(typed, BatchProgressEvent::Progress(ref p) if p.total == 5));
176 }
177
178 #[test]
179 fn parse_batch_completed_event() {
180 let event = RawSseEvent {
181 event_type: "batch_completed".to_string(),
182 data: r#"{"event":"batch_completed","status":"completed"}"#.to_string(),
183 id: None,
184 };
185 let result = parse_typed_event(event).expect("should produce event");
186 let typed = result.expect("should parse");
187 assert!(
188 matches!(typed, BatchProgressEvent::BatchCompleted(ref c) if c.status == "completed")
189 );
190 }
191
192 #[test]
193 fn parse_unknown_event_returns_none() {
194 let event = RawSseEvent {
195 event_type: "ping".to_string(),
196 data: "{}".to_string(),
197 id: None,
198 };
199 assert!(parse_typed_event(event).is_none());
200 }
201}