1use anyhow::Result;
2use quick_xml::events::Event;
3use quick_xml::Reader;
4use std::path::PathBuf;
5
6#[derive(Debug, Clone)]
7pub struct JobSummary {
8 pub job_id: String,
9 pub url: String,
10 pub tool: Option<String>,
11 pub job_stage: Option<String>,
12 pub failed: bool,
13 pub date_submitted: Option<String>,
14 pub date_completed: Option<String>,
15}
16
17#[derive(Debug, Clone)]
18pub struct JobStatus {
19 pub job_id: String,
20 pub job_stage: String,
21 pub failed: bool,
22 pub date_submitted: Option<String>,
23 pub date_completed: Option<String>,
24 pub tool_id: Option<String>,
25 pub self_uri: String,
26 pub results_uri: Option<String>,
27 pub messages: Vec<JobMessage>,
28}
29
30#[derive(Debug, Clone)]
31pub struct JobMessage {
32 pub stage: String,
33 pub text: String,
34 pub timestamp: Option<String>,
35}
36
37#[derive(Debug, Clone)]
38pub struct OutputFile {
39 pub filename: String,
40 pub download_uri: String,
41 pub size: u64,
42}
43
44#[derive(Debug, Clone)]
45pub struct DownloadedFile {
46 pub filename: String,
47 pub path: PathBuf,
48 pub size: u64,
49}
50
51pub fn parse_job_list(xml: &str) -> Result<Vec<JobSummary>> {
52 let mut reader = Reader::from_str(xml);
53 reader.config_mut().trim_text(true);
54
55 let mut jobs = Vec::new();
56 let mut buf = Vec::new();
57
58 let mut current_url = None;
60 let mut current_title = None;
61 let mut current_tool = None;
62 let mut current_stage = None;
63 let mut current_failed = false;
64 let mut current_date_submitted = None;
65 let mut current_date_completed = None;
66
67 let mut in_self_uri = false;
68 let mut in_job = false;
69 let mut current_tag = String::new();
70
71 loop {
72 match reader.read_event_into(&mut buf) {
73 Ok(Event::Start(e)) => {
74 let tag = String::from_utf8_lossy(e.name().as_ref()).to_string();
75 current_tag = tag.clone();
76
77 match tag.as_str() {
78 "jobstatus" => {
79 in_job = true;
80 current_url = None;
82 current_title = None;
83 current_tool = None;
84 current_stage = None;
85 current_failed = false;
86 current_date_submitted = None;
87 current_date_completed = None;
88 }
89 "selfUri" => in_self_uri = true,
90 _ => {}
91 }
92 }
93 Ok(Event::End(e)) => {
94 let tag = String::from_utf8_lossy(e.name().as_ref()).to_string();
95
96 match tag.as_str() {
97 "selfUri" => in_self_uri = false,
98 "jobstatus" => {
99 in_job = false;
100 if let (Some(url), Some(title)) = (current_url.take(), current_title.take())
101 {
102 jobs.push(JobSummary {
103 job_id: title,
104 url,
105 tool: current_tool.take(),
106 job_stage: current_stage.take(),
107 failed: current_failed,
108 date_submitted: current_date_submitted.take(),
109 date_completed: current_date_completed.take(),
110 });
111 }
112 current_failed = false;
113 }
114 _ => {}
115 }
116 current_tag.clear();
117 }
118 Ok(Event::Text(e)) => {
119 let text = reader
120 .decoder()
121 .decode(e.as_ref())
122 .ok()
123 .map(|s| s.to_string());
124
125 if let Some(text) = text {
126 match current_tag.as_str() {
127 "url" if in_self_uri => current_url = Some(text),
128 "title" if in_self_uri => current_title = Some(text),
129 "toolId" if in_job => current_tool = Some(text),
130 "jobStage" if in_job => current_stage = Some(text),
131 "failed" if in_job => current_failed = text == "true",
132 "dateSubmitted" if in_job => current_date_submitted = Some(text),
133 "dateTerminated" if in_job => current_date_completed = Some(text),
134 _ => {}
135 }
136 }
137 }
138 Ok(Event::Eof) => break,
139 Err(e) => {
140 return Err(anyhow::anyhow!(
141 "XML parse error at position {}: {}",
142 reader.buffer_position(),
143 e
144 ))
145 }
146 _ => {}
147 }
148 buf.clear();
149 }
150
151 Ok(jobs)
152}
153
154pub fn parse_job_status(xml: &str) -> Result<JobStatus> {
155 let mut reader = Reader::from_str(xml);
156 reader.config_mut().trim_text(true);
157
158 let mut buf = Vec::new();
159 let mut job_id = String::new();
160 let mut job_stage = String::new();
161 let mut failed = false;
162 let mut terminal_stage = false;
163 let mut date_submitted: Option<String> = None;
164 let mut date_completed: Option<String> = None;
165 let mut self_uri = String::new();
166 let mut results_uri: Option<String> = None;
167 let mut messages = Vec::new();
168
169 let mut current_tag = String::new();
170 let mut in_results_uri = false;
171 let mut in_message = false;
172 let mut current_message_stage = String::new();
173 let mut current_message_text = String::new();
174 let mut current_message_timestamp = None;
175
176 loop {
177 match reader.read_event_into(&mut buf) {
178 Ok(Event::Start(e)) => {
179 current_tag = String::from_utf8_lossy(e.name().as_ref()).to_string();
180 match current_tag.as_str() {
181 "resultsUri" => in_results_uri = true,
182 "message" => {
183 in_message = true;
184 current_message_stage.clear();
185 current_message_text.clear();
186 current_message_timestamp = None;
187 }
188 _ => {}
189 }
190 }
191 Ok(Event::End(e)) => {
192 let tag = String::from_utf8_lossy(e.name().as_ref()).to_string();
193 match tag.as_str() {
194 "resultsUri" => in_results_uri = false,
195 "message" => {
196 if in_message {
197 messages.push(JobMessage {
198 stage: current_message_stage.clone(),
199 text: current_message_text.clone(),
200 timestamp: current_message_timestamp.clone(),
201 });
202 in_message = false;
203 }
204 }
205 _ => {}
206 }
207 current_tag.clear();
208 }
209 Ok(Event::Text(e)) => {
210 let text = reader
211 .decoder()
212 .decode(e.as_ref())
213 .map(|s| s.to_string())
214 .unwrap_or_default();
215 match current_tag.as_str() {
216 "jobHandle" => job_id = text,
217 "jobStage" => job_stage = text,
218 "failed" => failed = text == "true",
219 "terminalStage" => terminal_stage = text == "true",
220 "dateSubmitted" => date_submitted = Some(text),
221 "url" if in_results_uri => results_uri = Some(text),
222 "url" if !in_results_uri && self_uri.is_empty() => self_uri = text,
223 "stage" if in_message => current_message_stage = text,
224 "text" if in_message => current_message_text = text,
225 "timestamp" if in_message => current_message_timestamp = Some(text),
226 _ => {}
227 }
228 }
229 Ok(Event::Eof) => break,
230 Err(e) => return Err(anyhow::anyhow!("XML parse error: {}", e)),
231 _ => {}
232 }
233 buf.clear();
234 }
235
236 if job_id.is_empty() {
237 anyhow::bail!("Failed to parse job status: missing job ID");
238 }
239
240 let extracted_tool = extract_tool_from_job_id(&job_id);
242
243 if terminal_stage && date_completed.is_none() && !messages.is_empty() {
245 date_completed = messages.last().and_then(|m| m.timestamp.clone());
246 }
247
248 Ok(JobStatus {
249 job_id,
250 job_stage,
251 failed,
252 date_submitted,
253 date_completed,
254 tool_id: extracted_tool,
255 self_uri,
256 results_uri,
257 messages,
258 })
259}
260
261fn extract_tool_from_job_id(job_id: &str) -> Option<String> {
262 let parts: Vec<&str> = job_id.split('-').collect();
266 if parts.len() >= 4 && parts[0] == "NGBW" && parts[1] == "JOB" {
267 let last_part = parts[parts.len() - 1];
268 if last_part.len() == 32 && last_part.chars().all(|c| c.is_ascii_hexdigit()) {
270 let tool_parts = &parts[2..parts.len() - 1];
272 Some(tool_parts.join("-"))
273 } else {
274 Some(parts[2].to_string())
276 }
277 } else {
278 None
279 }
280}
281
282pub fn parse_output_files(xml: &str) -> Result<Vec<OutputFile>> {
283 let mut reader = Reader::from_str(xml);
284 reader.config_mut().trim_text(true);
285
286 let mut files = Vec::new();
287 let mut buf = Vec::new();
288
289 let mut in_jobfile = false;
290 let mut in_download_uri = false;
291 let mut current_filename = None;
292 let mut current_download_uri = None;
293 let mut current_size = None;
294 let mut current_tag = String::new();
295
296 loop {
297 match reader.read_event_into(&mut buf) {
298 Ok(Event::Start(e)) => {
299 let tag = String::from_utf8_lossy(e.name().as_ref()).to_string();
300 match tag.as_str() {
301 "jobfile" => in_jobfile = true,
302 "downloadUri" => in_download_uri = true,
303 _ => current_tag = tag,
304 }
305 }
306 Ok(Event::End(e)) => {
307 let tag = String::from_utf8_lossy(e.name().as_ref()).to_string();
308 match tag.as_str() {
309 "jobfile" => {
310 if let (Some(filename), Some(download_uri), Some(size)) = (
311 current_filename.take(),
312 current_download_uri.take(),
313 current_size.take(),
314 ) {
315 files.push(OutputFile {
316 filename,
317 download_uri,
318 size,
319 });
320 }
321 in_jobfile = false;
322 }
323 "downloadUri" => in_download_uri = false,
324 _ => {}
325 }
326 current_tag.clear();
327 }
328 Ok(Event::Text(e)) => {
329 let text = reader
330 .decoder()
331 .decode(e.as_ref())
332 .map(|s| s.to_string())
333 .unwrap_or_default();
334 if in_jobfile {
335 match current_tag.as_str() {
336 "filename" => current_filename = Some(text),
337 "length" => current_size = text.parse().ok(),
338 "url" if in_download_uri => current_download_uri = Some(text),
339 _ => {}
340 }
341 }
342 }
343 Ok(Event::Eof) => break,
344 Err(e) => return Err(anyhow::anyhow!("XML parse error: {}", e)),
345 _ => {}
346 }
347 buf.clear();
348 }
349
350 Ok(files)
351}