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 {
63                        job_id: title,
64                        url,
65                    });
66                }
67            }
68            Ok(Event::Start(e)) if in_self_uri && e.name().as_ref() == b"url" => {
69                if let Ok(Event::Text(t)) = reader.read_event_into(&mut buf) {
70                    current_url = reader.decoder().decode(t.as_ref()).ok().map(|s| s.to_string());
71                }
72            }
73            Ok(Event::Start(e)) if in_self_uri && e.name().as_ref() == b"title" => {
74                if let Ok(Event::Text(t)) = reader.read_event_into(&mut buf) {
75                    current_title = reader.decoder().decode(t.as_ref()).ok().map(|s| s.to_string());
76                }
77            }
78            Ok(Event::Eof) => break,
79            Err(e) => return Err(anyhow::anyhow!("XML parse error at position {}: {}", reader.buffer_position(), e)),
80            _ => {}
81        }
82        buf.clear();
83    }
84
85    Ok(jobs)
86}
87
88pub fn parse_job_status(xml: &str) -> Result<JobStatus> {
89    let mut reader = Reader::from_str(xml);
90    reader.config_mut().trim_text(true);
91
92    let mut buf = Vec::new();
93    let mut job_id = String::new();
94    let mut job_stage = String::new();
95    let mut failed = false;
96    let mut date_submitted = None;
97    let mut self_uri = String::new();
98    let mut results_uri = None;
99    let mut messages = Vec::new();
100
101    let mut current_tag = String::new();
102    let mut in_results_uri = false;
103    let mut in_message = false;
104    let mut current_message_stage = String::new();
105    let mut current_message_text = String::new();
106    let mut current_message_timestamp = None;
107
108    loop {
109        match reader.read_event_into(&mut buf) {
110            Ok(Event::Start(e)) => {
111                current_tag = String::from_utf8_lossy(e.name().as_ref()).to_string();
112                match current_tag.as_str() {
113                    "resultsUri" => in_results_uri = true,
114                    "message" => {
115                        in_message = true;
116                        current_message_stage.clear();
117                        current_message_text.clear();
118                        current_message_timestamp = None;
119                    }
120                    _ => {}
121                }
122            }
123            Ok(Event::End(e)) => {
124                let tag = String::from_utf8_lossy(e.name().as_ref()).to_string();
125                match tag.as_str() {
126                    "resultsUri" => in_results_uri = false,
127                    "message" => {
128                        if in_message {
129                            messages.push(JobMessage {
130                                stage: current_message_stage.clone(),
131                                text: current_message_text.clone(),
132                                timestamp: current_message_timestamp.clone(),
133                            });
134                            in_message = false;
135                        }
136                    }
137                    _ => {}
138                }
139                current_tag.clear();
140            }
141            Ok(Event::Text(e)) => {
142                let text = reader.decoder().decode(e.as_ref())
143                    .map(|s| s.to_string())
144                    .unwrap_or_default();
145                match current_tag.as_str() {
146                    "jobHandle" => job_id = text,
147                    "jobStage" => job_stage = text,
148                    "failed" => failed = text == "true",
149                    "dateSubmitted" => date_submitted = Some(text),
150                    "url" if in_results_uri => results_uri = Some(text),
151                    "url" if !in_results_uri && self_uri.is_empty() => self_uri = text,
152                    "stage" if in_message => current_message_stage = text,
153                    "text" if in_message => current_message_text = text,
154                    "timestamp" if in_message => current_message_timestamp = Some(text),
155                    _ => {}
156                }
157            }
158            Ok(Event::Eof) => break,
159            Err(e) => return Err(anyhow::anyhow!("XML parse error: {}", e)),
160            _ => {}
161        }
162        buf.clear();
163    }
164
165    if job_id.is_empty() {
166        anyhow::bail!("Failed to parse job status: missing job ID");
167    }
168
169    Ok(JobStatus {
170        job_id,
171        job_stage,
172        failed,
173        date_submitted,
174        self_uri,
175        results_uri,
176        messages,
177    })
178}
179
180pub fn parse_output_files(xml: &str) -> Result<Vec<OutputFile>> {
181    let mut reader = Reader::from_str(xml);
182    reader.config_mut().trim_text(true);
183
184    let mut files = Vec::new();
185    let mut buf = Vec::new();
186
187    let mut in_jobfile = false;
188    let mut in_download_uri = false;
189    let mut current_filename = None;
190    let mut current_download_uri = None;
191    let mut current_size = None;
192    let mut current_tag = String::new();
193
194    loop {
195        match reader.read_event_into(&mut buf) {
196            Ok(Event::Start(e)) => {
197                let tag = String::from_utf8_lossy(e.name().as_ref()).to_string();
198                match tag.as_str() {
199                    "jobfile" => in_jobfile = true,
200                    "downloadUri" => in_download_uri = true,
201                    _ => current_tag = tag,
202                }
203            }
204            Ok(Event::End(e)) => {
205                let tag = String::from_utf8_lossy(e.name().as_ref()).to_string();
206                match tag.as_str() {
207                    "jobfile" => {
208                        if let (Some(filename), Some(download_uri), Some(size)) =
209                            (current_filename.take(), current_download_uri.take(), current_size.take())
210                        {
211                            files.push(OutputFile {
212                                filename,
213                                download_uri,
214                                size,
215                            });
216                        }
217                        in_jobfile = false;
218                    }
219                    "downloadUri" => in_download_uri = false,
220                    _ => {}
221                }
222                current_tag.clear();
223            }
224            Ok(Event::Text(e)) => {
225                let text = reader.decoder().decode(e.as_ref())
226                    .map(|s| s.to_string())
227                    .unwrap_or_default();
228                if in_jobfile {
229                    match current_tag.as_str() {
230                        "filename" => current_filename = Some(text),
231                        "length" => current_size = text.parse().ok(),
232                        "url" if in_download_uri => current_download_uri = Some(text),
233                        _ => {}
234                    }
235                }
236            }
237            Ok(Event::Eof) => break,
238            Err(e) => return Err(anyhow::anyhow!("XML parse error: {}", e)),
239            _ => {}
240        }
241        buf.clear();
242    }
243
244    Ok(files)
245}