nsg_cli/
models.rs

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}
11
12#[derive(Debug, Clone)]
13pub struct JobStatus {
14    pub job_id: String,
15    pub job_stage: String,
16    pub failed: bool,
17    pub date_submitted: Option<String>,
18    pub self_uri: String,
19    pub results_uri: Option<String>,
20    pub messages: Vec<JobMessage>,
21}
22
23#[derive(Debug, Clone)]
24pub struct JobMessage {
25    pub stage: String,
26    pub text: String,
27    pub timestamp: Option<String>,
28}
29
30#[derive(Debug, Clone)]
31pub struct OutputFile {
32    pub filename: String,
33    pub download_uri: String,
34    pub size: u64,
35}
36
37#[derive(Debug, Clone)]
38pub struct DownloadedFile {
39    pub filename: String,
40    pub path: PathBuf,
41    pub size: u64,
42}
43
44pub fn parse_job_list(xml: &str) -> Result<Vec<JobSummary>> {
45    let mut reader = Reader::from_str(xml);
46    reader.config_mut().trim_text(true);
47
48    let mut jobs = Vec::new();
49    let mut buf = Vec::new();
50    let mut current_url = None;
51    let mut current_title = None;
52    let mut in_self_uri = false;
53
54    loop {
55        match reader.read_event_into(&mut buf) {
56            Ok(Event::Start(e)) if e.name().as_ref() == b"selfUri" => {
57                in_self_uri = true;
58            }
59            Ok(Event::End(e)) if e.name().as_ref() == b"selfUri" => {
60                in_self_uri = false;
61                if let (Some(url), Some(title)) = (current_url.take(), current_title.take()) {
62                    jobs.push(JobSummary { job_id: title, url });
63                }
64            }
65            Ok(Event::Start(e)) if in_self_uri && e.name().as_ref() == b"url" => {
66                if let Ok(Event::Text(t)) = reader.read_event_into(&mut buf) {
67                    current_url = reader
68                        .decoder()
69                        .decode(t.as_ref())
70                        .ok()
71                        .map(|s| s.to_string());
72                }
73            }
74            Ok(Event::Start(e)) if in_self_uri && e.name().as_ref() == b"title" => {
75                if let Ok(Event::Text(t)) = reader.read_event_into(&mut buf) {
76                    current_title = reader
77                        .decoder()
78                        .decode(t.as_ref())
79                        .ok()
80                        .map(|s| s.to_string());
81                }
82            }
83            Ok(Event::Eof) => break,
84            Err(e) => {
85                return Err(anyhow::anyhow!(
86                    "XML parse error at position {}: {}",
87                    reader.buffer_position(),
88                    e
89                ))
90            }
91            _ => {}
92        }
93        buf.clear();
94    }
95
96    Ok(jobs)
97}
98
99pub fn parse_job_status(xml: &str) -> Result<JobStatus> {
100    let mut reader = Reader::from_str(xml);
101    reader.config_mut().trim_text(true);
102
103    let mut buf = Vec::new();
104    let mut job_id = String::new();
105    let mut job_stage = String::new();
106    let mut failed = false;
107    let mut date_submitted = None;
108    let mut self_uri = String::new();
109    let mut results_uri = None;
110    let mut messages = Vec::new();
111
112    let mut current_tag = String::new();
113    let mut in_results_uri = false;
114    let mut in_message = false;
115    let mut current_message_stage = String::new();
116    let mut current_message_text = String::new();
117    let mut current_message_timestamp = None;
118
119    loop {
120        match reader.read_event_into(&mut buf) {
121            Ok(Event::Start(e)) => {
122                current_tag = String::from_utf8_lossy(e.name().as_ref()).to_string();
123                match current_tag.as_str() {
124                    "resultsUri" => in_results_uri = true,
125                    "message" => {
126                        in_message = true;
127                        current_message_stage.clear();
128                        current_message_text.clear();
129                        current_message_timestamp = None;
130                    }
131                    _ => {}
132                }
133            }
134            Ok(Event::End(e)) => {
135                let tag = String::from_utf8_lossy(e.name().as_ref()).to_string();
136                match tag.as_str() {
137                    "resultsUri" => in_results_uri = false,
138                    "message" => {
139                        if in_message {
140                            messages.push(JobMessage {
141                                stage: current_message_stage.clone(),
142                                text: current_message_text.clone(),
143                                timestamp: current_message_timestamp.clone(),
144                            });
145                            in_message = false;
146                        }
147                    }
148                    _ => {}
149                }
150                current_tag.clear();
151            }
152            Ok(Event::Text(e)) => {
153                let text = reader
154                    .decoder()
155                    .decode(e.as_ref())
156                    .map(|s| s.to_string())
157                    .unwrap_or_default();
158                match current_tag.as_str() {
159                    "jobHandle" => job_id = text,
160                    "jobStage" => job_stage = text,
161                    "failed" => failed = text == "true",
162                    "dateSubmitted" => date_submitted = Some(text),
163                    "url" if in_results_uri => results_uri = Some(text),
164                    "url" if !in_results_uri && self_uri.is_empty() => self_uri = text,
165                    "stage" if in_message => current_message_stage = text,
166                    "text" if in_message => current_message_text = text,
167                    "timestamp" if in_message => current_message_timestamp = Some(text),
168                    _ => {}
169                }
170            }
171            Ok(Event::Eof) => break,
172            Err(e) => return Err(anyhow::anyhow!("XML parse error: {}", e)),
173            _ => {}
174        }
175        buf.clear();
176    }
177
178    if job_id.is_empty() {
179        anyhow::bail!("Failed to parse job status: missing job ID");
180    }
181
182    Ok(JobStatus {
183        job_id,
184        job_stage,
185        failed,
186        date_submitted,
187        self_uri,
188        results_uri,
189        messages,
190    })
191}
192
193pub fn parse_output_files(xml: &str) -> Result<Vec<OutputFile>> {
194    let mut reader = Reader::from_str(xml);
195    reader.config_mut().trim_text(true);
196
197    let mut files = Vec::new();
198    let mut buf = Vec::new();
199
200    let mut in_jobfile = false;
201    let mut in_download_uri = false;
202    let mut current_filename = None;
203    let mut current_download_uri = None;
204    let mut current_size = None;
205    let mut current_tag = String::new();
206
207    loop {
208        match reader.read_event_into(&mut buf) {
209            Ok(Event::Start(e)) => {
210                let tag = String::from_utf8_lossy(e.name().as_ref()).to_string();
211                match tag.as_str() {
212                    "jobfile" => in_jobfile = true,
213                    "downloadUri" => in_download_uri = true,
214                    _ => current_tag = tag,
215                }
216            }
217            Ok(Event::End(e)) => {
218                let tag = String::from_utf8_lossy(e.name().as_ref()).to_string();
219                match tag.as_str() {
220                    "jobfile" => {
221                        if let (Some(filename), Some(download_uri), Some(size)) = (
222                            current_filename.take(),
223                            current_download_uri.take(),
224                            current_size.take(),
225                        ) {
226                            files.push(OutputFile {
227                                filename,
228                                download_uri,
229                                size,
230                            });
231                        }
232                        in_jobfile = false;
233                    }
234                    "downloadUri" => in_download_uri = false,
235                    _ => {}
236                }
237                current_tag.clear();
238            }
239            Ok(Event::Text(e)) => {
240                let text = reader
241                    .decoder()
242                    .decode(e.as_ref())
243                    .map(|s| s.to_string())
244                    .unwrap_or_default();
245                if in_jobfile {
246                    match current_tag.as_str() {
247                        "filename" => current_filename = Some(text),
248                        "length" => current_size = text.parse().ok(),
249                        "url" if in_download_uri => current_download_uri = Some(text),
250                        _ => {}
251                    }
252                }
253            }
254            Ok(Event::Eof) => break,
255            Err(e) => return Err(anyhow::anyhow!("XML parse error: {}", e)),
256            _ => {}
257        }
258        buf.clear();
259    }
260
261    Ok(files)
262}